如何在Flutter中编写一个可横切式插入多个Widget的灵活列表


前言

本篇文章使用回调函数来实现如何编写一个可横切式插入多个 Widget 的灵活列表。所以,读完本篇文章之后,你可以掌握如何在 Dart 中定义回调函数,以及如何利用回调函数封装一个高可用的 Widget。

主要启发来源于 Element 组件库中的表格组件。提供一个数组,其元素是一个个对象,对象字段随意。使用组件时,给传递一个属性 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 createState() => _ActionableListState();
}

class _ActionableListState extends State {
  @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 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 _createList() {
  List 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 {
  @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 data;

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

  @override
  State 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 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 data;
  final List 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 createState() => _ActionableListState();
}

class _ActionableListState extends State {
  Widget _createItems(
    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: Padding(
                padding: const EdgeInsets.only(right: 5),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: [middle],
                ),
              ),
            ),
            Icon(icon),
          ],
        ),
      ),
    );
  }

  List _createList() {
    List list = [];
    for (int i = 0; i < widget.data.length; i++) {
      list.add(
        _createItems(
          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 createState() => _UserCenterSliceState();
}

class _UserCenterSliceState extends State {
  Map 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),
          )
        ],
      ),
    );
  }
}