白筱汐

想都是问题,做都是答案

0%

flutter异步UI更新

异步UI更新

很多时候我们会依赖一些异步数据来动态更新UI,比如在打开一个页面时我们需要先从互联网上获取数据,在获取数据的过程中我们显示一个加载框,等获取到数据时我们再渲染页面;又比如我们想展示Stream(比如文件流、互联网数据接收流)的进度。

之前已经介绍过 Dart 中的异步操作主要是 Future 和 Stream,也介绍了相关的用法,这片文章将介绍与之对应的构建异步UI的两个方法 FutureBuilder 与 StreamBuilder。

FutureBuilder

Future 必须在更早之前获取,例如在State.initState、State.didUpdateWidget 或 State.didChangeDependencies 期间得到。它不能在构造 FutureBuilder 时调用 State.build 或 StatelessWidget.build 方法时创建。如果 Future 是与 FutureBuilder 同时创建的,那么每当 FutureBuilder 的父节点重建时,异步任务就会重新启动。

构建方法:

1
2
3
4
5
6
7
8
9
10
11
const FutureBuilder<T>({
Key? key,
required Future<T>? future, // FutureBuilder依赖的Future,通常是一个异步耗时任务
T? initialData, // 初始数据,用户设置默认数据
required AsyncWidgetBuilder<T> builder // Widget构建器;该构建器会在Future执行的不同阶段被多次调用
})

AsyncWidgetBuilder<T> = Widget Function(
BuildContext context,
AsyncSnapshot<T> snapshot
)

snapshot会包含当前异步任务的状态信息及结果信息 ,比如我们可以通过snapshot.connectionState获取异步任务的状态信息, 通过snapshot.hasError判断异步任务是否有错误等等,完整的定义读者可以查看AsyncSnapshot类定义。另外,FutureBuilder的builder函数签名和StreamBuilder的builder是相同的。

查看源码可知 snapshot.connectionState 属性是一个枚举,用于表示异步操作的连接状态:

  • ConnectionState.none:表示异步操作未启动,或者 Future 对象尚未创建。
  • ConnectionState.waiting:表示异步操作正在进行中,等待 Future 完成。
  • ConnectionState.active:(用于Stream)表示数据流处于活动状态,已经开始发送事件。在接收到第一个事件之后,connectionState 的值将变为 active,但是还没有完成。
  • ConnectionState.done:表示异步操作已经完成或抛出了异常。

可以根据状态返回不同的UI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FutureBuilder<String>(
future: myFuture,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return Text('Data: ${snapshot.data}');
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
}

return Text('Press the button to start');
},
)

完整代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 以下代码在 State 中

late Future<List<Album>> futureAlbums;

// 这是一段从接口获取数据,并转换成 List<Album> 的方法
Future<List<Album>> fetchAlbum() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/albums'),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
HttpHeaders.authorizationHeader: 'Basic your_api_token_here',
},
);

if (response.statusCode == 200) {
// 如果服务器返回了 200 OK 响应,则解析 JSON。
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((json) => Album.fromJson(json)).toList();
} else {
// 如果服务器未返回 200 OK 响应,则抛出异常。
throw Exception('Failed to load album');
}
}

@override
void initState() {
super.initState();
futureAlbums = fetchAlbum(); // 在 initState 时获取 Future
}

...

Widget build(BuildContext context) {
return Center(
child: FutureBuilder<List<Album>>(
future: futureAlbums,
builder: (context, AsyncSnapshot snapshot) {
// 有数据
if (snapshot.hasData) {
return Scrollbar(
child: ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(snapshot.data![index].title),
);
},
));
// 发生错误
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// 默认情况下展示一个加载中的进度条
return const CircularProgressIndicator();
},
);
);
}

StreamBuilder

我们知道,在Dart中Stream 也是用于接收异步事件数据,和Future 不同的是,它可以接收多个异步操作的结果,它常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。StreamBuilder正是用于配合Stream来展示流上事件(数据)变化的UI组件。

构建方法:

1
2
3
4
5
6
const StreamBuilder<T>({
Key? key,
T? initialData, // 初始数据,用户设置默认数据
required Stream<T>? stream, // 依赖的一个 Stream
required AsyncWidgetBuilder<T> builder // 与 FutureBuilder 相同,只是 connectionState 中的 active 状态需要特殊注意
})

创建一个异步数据流,每隔1秒发送一个数据,获取10个数字(0-9)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 以下代码在 State 中
final Stream<int> counterStream = Stream.periodic(const Duration(seconds: 1), (count) => count).take(10);

final broadcastStreamController = StreamController<int>.broadcast(); // 广播类型的 Stream,可以被多个监听

@override
void initState() {
super.initState();
futureAlbums = fetchAlbum();
// pipe() 是 Stream 类的一个方法,用于将一个流连接到另一个流。它会将源流中的事件转发到目标流中,以实现数据传递
counterStream.pipe(broadcastStreamController);
}

Widget build(BuildContext context) {
return Center(
child: StreamBuilder<int>(
stream: broadcastStreamController.stream,
builder: (context, AsyncSnapshot<int> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return const Text('waiting');
case ConnectionState.none:
return const Text('none');
case ConnectionState.active:
// 每次数据发送都会重新 build,更新ui
return Text('Count: ${snapshot.data}');
case ConnectionState.done:
return const Text('done');
}
},
);

);
}