1. 程式人生 > 其它 >如何在Flutter中編寫一個可橫切式插入多個Widget的靈活列表

如何在Flutter中編寫一個可橫切式插入多個Widget的靈活列表

前言

本篇文章使用回撥函式來實現如何編寫一個可橫切式插入多個 Widget 的靈活列表。所以,讀完本篇文章之後,你可以掌握如何在 Dart 中定義回撥函式,以及如何利用回撥函式封裝一個高可用的 Widget。

主要啟發來源於 Element 元件庫中的表格元件。提供一個數組,其元素是一個個物件,物件欄位隨意。使用元件時,給<el-table-column>傳遞一個屬性 prop,其對應著tableData陣列元素的欄位名。

正題

定義回撥函式

在此之前,必須要了解在 Dart 中如何定義回撥函式。

首先,使用typedef關鍵字宣告一個函式的別名:

typedef OnCreated = void Function(String e);

注意:函式的別名宣告的位置最好在類之外,且.dart檔案之內。

在 Person 類中的 create 函式裡建立回撥函式:

class Person {
  void create(String e, OnCreated callback) {
    callback(e);
  }
}

main函式使用 create 方法:

void main() {
  Person().create('hello world!', (e) { print(e); });
}

建立列表元件

通過本篇文章實現如圖所示的列表元件:

建立 StatefulWidget

將元件命名為 ActionableList,意思是可操作的列表,由於列表的項中間部分的內容可能隨著使用者修改而修改,所以定義為 StatefulWidget。

class ActionableList extends StatefulWidget {
  const ActionableList({Key? key}) : super(key: key);

  @override
  State<ActionableList> createState() => _ActionableListState();
}

class _ActionableListState extends State<ActionableList> {
  @override
  Widget build(BuildContext context) {
    return Column();
  }
}

ActionableListTemplate

列表元件下有許多項,每一項的佈局是左中右,左邊為 Text 元件,中間為 Widget 型別的元件,右邊為 Icon 元件,整個列表的項是可以被點選的。

因此,ActionableListTemplate 就被用作約束使用者以什麼結構來渲染列表的每一項。

class ActionableListTemplate {
  final String label;
  final String middle;
  final IconData icon;

  ActionableListTemplate({
    required this.label,
    required this.middle,
    this.icon = Icons.arrow_forward_ios
  });
}

建立好 ActionableListTemplate 之後,要求使用者使用 ActionableList 元件時必須傳遞一個 ActionableListTemplate 的引數:

class ActionableList extends StatefulWidget {
  final List<ActionableListTemplate> template;

  const ActionableList({Key? key, required this.template}) : super(key: key);
}

構建 ActionableList 的介面

實現 ActionableList 的 UI,在 build 函式中,為了時程式碼更有閱讀性,每一個步驟抽取到一個函式中:

第一步,建立列表的一個項:

Widget _createItem(String label, Widget middle, IconData icon) {
  return InkWell(
    child: Padding(
      child: Row(
        children: [
          Text(
            label,
            style: TextStyle(color: widget.labelColor),
          ),
          Expanded(child: middle),
          Icon(icon),
        ],
      ),
    ),
  );
}

第二步,建立列表:

List<Widget> _createList() {
  List<Widget> list = [];
  for (int i = 0; i < widget.template.length; i++) {
    list.add(
      _createItem(widget.template[i].label, widget.template[i].middle, widget.template[i].icon));
  }
  return list;
}

build 函式只需要呼叫 _createList 函式建立一個列表即可:

@override
Widget build(BuildContext context) {
  return Column(
    children: _createList(),
  );
}

使用 ActionableList

class _UserCenterSliceState extends State<UserCenterSlice> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ActionableList(
        template: [
          ActionableListTemplate(
            label: '頭像',
            middle: Avatar(url: 'assets/images/icon')
          ),
          ActionableListTemplate(
            label: '暱稱',
            content: Text('shiramashiro')
          ),
          ActionableListTemplate(
            label: '性別',
            content: Text('男'),
          ),
        ],
    );
  }
}

缺陷分析

雖然 ActionableListTemplate 的 content 屬性可以插入各式各樣的 Widget,但是這些 Widget 內的字串、數值等資料無法根據業務需求而靈活地變更。一般,這些資料都是來源於請求得來的 JSON 格式資料。

請看下面給出的簡單例子:

假如有一個 JSON 資料,其中一個欄位為 hobbies,有的使用者有三個、有的使用者有四個等等情況,資料不是死的,而是靈活的。

final jsonData = {
  hobbies: [ '打籃球', '看小說', '程式設計' ]
}

....

ActionableListTemplate(
  label: '興趣',
  content: Row(children: [ Text('打籃球'), Text('看小說'), Text('程式設計') ])
),

也可以在使用元件的時候,專門寫一個函式對該欄位進行迴圈。也是可以的,但是不優雅,不“好看”。

改進思路

更好的方式就是,把請求過來的資料直接交給 ActionableList 管理。首先,ActionableList 肯定是要通過 ActionableListTemplate 構建列表;其次,在 content 欄位這裡,可以更加靈活一點,比如 A 頁面使用了列表元件,利用回撥函式把對應的欄位返回到 A 頁面這一層面中,在 A 頁面裡寫邏輯、寫函式、寫 Widget 等等。

改造

新增新的屬性

在類中為其建構函式新增一個引數:

class ActionableList extends StatefulWidget {
  ...

  final Map<dynamic, dynamic> data;

  const ActionableList({Key? key, required this.data, ...}) : super(key: key);

  @override
  State<ActionableList> createState() => _ActionableListState();
}

添加回調函式

在類外部定義一個回撥函式,返回型別為 Widget,並且接收一個 dynamic 型別的引數,這個引數可以被外部獲得:

typedef Created = Widget Function(dynamic e);

修改屬性

ActionableListTemplate 新增屬性,並將原本的 content 屬性改名為 String 型別的 filed 屬性:

class ActionableListTemplate {
  ...
  final String field; // 原本是 Widget 型別,現在是 String 型別。
  ...
  final Created created; // created 將作為回撥函式返回 filed 對應的 JSON 資料。

  ActionableListTemplate({
    ...
    required this.field,
    ...
    required this.created,
  });
}

修改 _createItem

在 _createItem 函式中新增一個引數:

Widget _createItems(
  ...
  String filed,
  ...
  Created created, // 新增引數
) { 
  Widget middle = created(filed); // 把 filed 屬性傳給 created 回撥函式,在外部可以通過回撥函式取到該值。
  ...
  return (
    ...
    Expanded(child: middle),
  );
}

created 回撥函式在往外傳遞資料時,也將得到一個 Widget 型別的變數,然後將其插入到 Expanded(child: middle) 中。

效果示範

第一步,提供一個 Map 型別的資料:

Map<String, dynamic> data = {
  'uname': '椎名白白',
  'sex': '男',
  'avatar': 'assets/images/95893409_p0.jpg'
};

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: ActionableList(
      data: data,
      template: [
        ActionableListTemplate(
          label: '頭像',
          field: 'avatar',
          created: (e) => Avatar(url: e, size: 50), // e 就是 data['avatar']
        ),
        ActionableListTemplate(
          label: '暱稱',
          field: 'uname',
          created: (e) => Text(e), // e 就是 data['uname']
        ),
        ActionableListTemplate(
          label: '性別',
          field: 'sex',
          created: (e) => Text(e),
        ),
      ],
    ),
  );
}

效果就是,提供一個 JSON 格式資料給 ActionableList,然後為 ActionableListTemplate 指定一個 filed 屬性,其對應這 JSON 的每一個欄位。最後,如何構造列表項中間的 Widget,由 A 頁面這裡提供,也就是在 created 回撥函式裡構建,並且能夠把對應的值給插入到任何位置。

完整示例

actionable_list.dart:

import 'package:flutter/material.dart';

typedef OnTap = void Function();
typedef Created = Widget Function(dynamic e);

class ActionableListTemplate {
  final String label;
  final String field;
  final IconData icon;
  final OnTap onTap;
  final Created created;

  ActionableListTemplate({
    required this.label,
    required this.field,
    this.icon = Icons.arrow_forward_ios,
    required this.onTap,
    required this.created,
  });
}

class ActionableList extends StatefulWidget {
  final Map<dynamic, dynamic> data;
  final List<ActionableListTemplate> template;
  final double top;
  final double left;
  final double right;
  final double bottom;
  final Color labelColor;

  const ActionableList({
    Key? key,
    required this.data,
    required this.template,
    this.top = 10,
    this.right = 10,
    this.left = 10,
    this.bottom = 10,
    this.labelColor = Colors.black,
  }) : super(key: key);

  @override
  State<ActionableList> createState() => _ActionableListState();
}

class _ActionableListState extends State<ActionableList> {
  Widget _createItems(
    int id,
    String label,
    String filed,
    IconData icon,
    OnTap onTap,
    Created created,
  ) {
    Widget middle = created(filed);
    return InkWell(
      onTap: onTap,
      child: Padding(
        padding: EdgeInsets.only(
          left: widget.left,
          top: widget.top,
          right: widget.right,
          bottom: widget.bottom,
        ),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text(
              label,
              style: TextStyle(
                color: widget.labelColor,
              ),
            ),
            Expanded(child: middle),
            Icon(icon),
          ],
        ),
      ),
    );
  }

  List<Widget> _createList() {
    List<Widget> list = [];
    for (int i = 0; i < widget.data.length; i++) {
      list.add(
        _createItems(
          i,
          widget.template[i].label,
          widget.data[widget.template[i].field],
          widget.template[i].icon,
          widget.template[i].onTap,
          widget.template[i].created,
        ),
      );
    }
    return list;
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: _createList(),
    );
  }
}

user_center_slice.dart:

import 'package:flutter/material.dart';
import 'package:qingyuo_mobile/components/actionable_list.dart';
import 'package:qingyuo_mobile/components/avatar.dart';

class UserCenterSlice extends StatefulWidget {
  const UserCenterSlice({Key? key}) : super(key: key);

  @override
  State<UserCenterSlice> createState() => _UserCenterSliceState();
}

class _UserCenterSliceState extends State<UserCenterSlice> {
  Map<String, dynamic> data = {
    'uname': '椎名白白',
    'sex': '男',
    'signature': 'Time tick away, dream faded away!',
    'uid': '7021686',
    'avatar': 'assets/images/95893409_p0.jpg'
  };

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: const Color.fromRGBO(147, 181, 207, 6),
        title: const Text("賬號資料"),
      ),
      body: ActionableList(
        data: data,
        template: [
          ActionableListTemplate(
            label: '頭像',
            field: 'avatar',
            onTap: () {},
            created: (e) => Avatar(url: e, size: 50),
          ),
          ActionableListTemplate(
            label: '暱稱',
            field: 'uname',
            onTap: () {},
            created: (e) => Text(e),
          ),
          ActionableListTemplate(
            label: '性別',
            field: 'sex',
            onTap: () {},
            created: (e) => Text(e),
          ),
          ActionableListTemplate(
            label: '個性簽名',
            field: 'signature',
            onTap: () {},
            created: (e) => Text(e),
          ),
          ActionableListTemplate(
            label: 'UID',
            field: 'uid',
            onTap: () {},
            created: (e) => Text(e),
          )
        ],
      ),
    );
  }
}