如何在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),
)
],
),
);
}
}