
在现代 Flutter 应用中,异步操作无处不在------从网络请求、数据库读写到文件 IO。然而,管理这些异步操作的状态常常是开发中的一大痛点。加载中、成功、失败这三种状态的切换,以及随之而来的重试、取消、并发控制等逻辑,很容易让我们的代码变得臃肿和混乱。
幸运的是,signals 状态管理库提供了专门为异步场景设计的强大工具,让我们能够以一种更声明式、更优雅的方式来处理这些复杂性。本文将深入探讨如何使用 signals 来精通 Flutter 中的异步状态管理。
一、为什么要在异步场景下思考状态管理
几乎每个应用都离不开异步操作,例如:
- 通过 API 从服务器获取数据
- 读写本地数据库或文件
- 与原生平台进行方法通道(MethodChannel)通信
- 监听 WebSocket 或其他数据流
传统的状态管理方式(如 setState 、ValueNotifier)在处理这些场景时,往往会遇到以下痛点:
- 状态切换耦合 :isLoading 、data 、error 这几个状态通常作为独立的变量散落在代码中,需要手动维护它们之间的互斥关系,容易出错。
- 手动控制繁琐 :每次请求开始时,都需要手动设置 isLoading = true 、error = null ,请求结束后再根据成功或失败去更新 data 或 error ,并设置 isLoading = false。
- UI 层判断繁杂 :在 UI 代码中,会出现大量的
if (isLoading) ... else if (error != null) ... else ...
的逻辑分支,降低了代码的可读性。 - 并发请求冲突:当用户快速触发多次请求(如快速点击按钮)时,容易出现竞态条件(Race Condition),旧的请求结果可能会覆盖新的请求结果。
- 重试/取消逻辑复杂:实现下拉刷新、点击重试、或者在页面销毁时取消请求,都需要编写额外的模板代码。
signals 通过提供专门的异步 API,旨在将这些通用逻辑进行高度封装,让你能更专注于业务本身。
二、Signals 中的异步支持:futureSignal / streamSignal / AsyncState
signals 的核心思想是将异步操作本身也视为一种响应式状态。为此,它提供了几个关键的 API。
1. AsyncState:异步三态的优雅抽象
AsyncState 是一个密封类(Sealed Class),专门用于表示一个异步操作的生命周期,它有三种具体状态:
- AsyncLoading:加载中状态。
- AsyncData :成功状态,并持有数据 T。
- AsyncError:失败状态,并持有错误信息和堆栈。
由于是密封类,我们可以配合 Dart的switch表达式 在 UI 层进行详尽的状态匹配,确保每种状态都被处理,杜绝遗漏。
2. futureSignal(...) :封装单次异步调用
futureSignal 是最常用的异步信号,它接收一个返回 Future 的函数,并自动管理这个 Future 的整个生命周期。它非常适合那些不需要动态改变参数的单次请求场景。
dart
// 定义一个获取用户信息的 Future
Future<String> fetchUserData() async {
await Future.delayed(const Duration(seconds: 2));
if (Random().nextBool()) {
return 'Hello, Signals!';
} else {
throw Exception('Failed to load user data');
}
}
// 创建一个 futureSignal
final user = futureSignal(() => fetchUserData());
// user 的值会自动在 AsyncLoading -> AsyncData / AsyncError 之间转换
它的核心作用是:
- 自动状态转换 :调用时,user 的值立即变为 AsyncLoading 。Future 完成后,值会根据结果变为 AsyncData 或 AsyncError。
- 内置刷新机制 :可以通过调用 user.refresh() 或 user.reload() 来重新执行这个 Future。
3. streamSignal(...):封装一个数据流
与 futureSignal 类似,streamSignal 用于封装一个 Stream。它会监听这个流,并随着流推送新数据或错误而更新自身的状态。
scss
// 定义一个每秒发出一个数字的 Stream
Stream<int> counterStream() {
return Stream.periodic(const Duration(seconds: 1), (i) => i);
}
// 创建一个 streamSignal
final counter = streamSignal(() => counterStream());
// counter 的值会随着时间依次变为 AsyncData(0), AsyncData(1), ...
4. asyncSignal
asyncSignal 是 futureSignal 和 streamSignal 底层依赖的基础信号。它是一个可以让你手动设置其 AsyncState 值 的可变信号。当我们处理更复杂的异步逻辑,比如需要动态参数的分页加载时,asyncSignal 会比 futureSignal 更灵活。
5. 关系与设计注意
-
关系 :futureSignal 和 streamSignal 都是一种特殊的 ReadonlySignal<AsyncState> 。你可以读取它们的值,也可以在 computed 和 effect 中依赖它们。
-
刷新 (refresh vs reload) :
- .refresh() :在重新请求时,如果之前有成功的数据(AsyncData ),它会保留旧数据,并将状态更新为 AsyncData 的 reloading 状态。
- .reload() :会彻底重置状态,立即进入 AsyncLoading 状态,清除旧数据。
-
并发控制 :当对同一个 futureSignal 实例 连续调用 .refresh() 或 .reload() 时,它有助于处理竞态条件,通常只有最后一次调用的结果会生效。但需要注意,这并不意味着所有异步场景的并发都是自动安全的,开发者在复杂逻辑中仍需谨慎处理。
三、异步信号在 Flutter UI 层中的用法
1. 创建异步信号
在 StatefulWidget 中配合 SignalsMixin 来创建和管理信号。
scala
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';
class UserProfile extends StatefulWidget {
const UserProfile({super.key});
@override
State<UserProfile> createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> with SignalsMixin {
// 使用 late final 在 state 中创建信号
late final user = futureSignal(() => fetchUserData());
// ... UI a
}
2. 根据不同状态渲染 UI
使用 signal.watch(context) 来监听信号变化。结合 Dart的 switch 表达式,可以写出非常清晰和类型安全的条件渲染逻辑。
less
@override
Widget build(BuildContext context) {
final userState = user.watch(context);
return Scaffold(
appBar: AppBar(title: const Text('Async Signals Demo')),
body: Center(
// 使用 switch 表达式进行模式匹配,取代非标准的 .when()
child: switch (userState) {
AsyncData(:final value) => Text('Welcome, $value!', style: const TextStyle(fontSize: 24)),
AsyncError(:final error) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Oops! $error'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => user.reload(),
child: const Text('Retry'),
),
],
),
// AsyncLoading 和其他可能的状态(如 initial)统一处理
_ => const CircularProgressIndicator(),
},
),
// ...
);
}
四、一个完整示例:用 Signals 做一个网络列表/分页/下拉刷新
这是将所有知识点融会贯通的最佳实践。我们将创建一个支持下拉刷新和上拉加载更多的列表。
假设你有一个后端 API,每页返回若干条数据。你要在 App 里做一个列表:初次加载第一页、下拉刷新重载、滑动到底部加载下一页、错误重试、以及没有更多数据的提示。下面代码展示如何用 asyncSignal 与普通 signal、computed 组合来实现这个功能。 模拟 API 服务:
dart
import 'dart:math';
class ApiService {
/// 模拟一个异步请求:获取某页的数据
Future<List<String>> fetchPage(int page, {int pageSize = 20}) async {
await Future.delayed(const Duration(milliseconds: 800));
// 假设最多 5 页
if (page > 5) {
return []; // 没有更多数据
}
// 随机模拟失败(比如 page == 3 时偶尔抛错)
if (page == 3 && Random().nextBool()) {
throw Exception("Network error on page 3");
}
// 返回列表
return List.generate(pageSize, (index) {
final itemIndex = (page - 1) * pageSize + index + 1;
return "Item #$itemIndex";
});
}
}
分页控制器:
ini
import 'package:signals/signals.dart';
import 'api_service.dart';
class PaginatedController {
final ApiService api;
PaginatedController({required this.api});
final signal<List<String>> items = signal<List<String>>([]);
final signal<int> _page = signal<int>(1);
final signal<bool> hasMore = signal<bool>(true);
// 初始状态设为 loading(也可以用其它初始值,比如 AsyncData(null))
final AsyncSignal<void> loader = asyncSignal<void>(AsyncState.loading());
late final computed<bool> isLoadingFirstPage = computed(() {
final st = loader.value;
return items.value.isEmpty && (st is AsyncLoading<void>);
});
Future<void> loadNextPage() async {
final st = loader.value;
if (st is AsyncLoading<void> || !hasMore.value) {
return;
}
// 用内建 API reload 或 refresh,根据业务语义
await loader.reload(); // 或者 await loader.refresh();
try {
final newList = await api.fetchPage(_page.value);
if (newList.isEmpty) {
hasMore.value = false;
} else {
items.value = [...items.value, ...newList];
_page.value = _page.value + 1;
}
// 成功
loader.value = AsyncState.data(null);
} catch (e, stTrace) {
loader.value = AsyncState.error(e, stTrace);
}
}
Future<void> refresh() async {
hasMore.value = true;
_page.value = 1;
await loader.reload(); // 或 refresh
try {
final newList = await api.fetchPage(_page.value);
items.value = newList;
_page.value = 2;
loader.value = AsyncState.data(null);
} catch (e, stTrace) {
items.value = [];
loader.value = AsyncState.error(e, stTrace);
}
}
void dispose() {
items.dispose();
_page.dispose();
hasMore.dispose();
loader.dispose();
isLoadingFirstPage.dispose();
}
}
在UI中使用:
less
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';
import 'paginated_controller.dart';
class ItemsPage extends StatefulWidget {
const ItemsPage({super.key});
@override
State<ItemsPage> createState() => _ItemsPageState();
}
class _ItemsPageState extends State<ItemsPage> with SignalsMixin {
late final controller = PaginatedController(api: ApiService());
final ScrollController scrollCtrl = ScrollController();
@override
void initState() {
super.initState();
controller.loadNextPage();
scrollCtrl.addListener(() {
final pos = scrollCtrl.position;
if (pos.pixels >= pos.maxScrollExtent - 200) {
controller.loadNextPage();
}
});
}
@override
void dispose() {
controller.dispose();
scrollCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (controller.isLoadingFirstPage.watch(context)) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
final itemList = controller.items.watch(context);
return Scaffold(
appBar: AppBar(title: const Text("Paginated List")),
body: RefreshIndicator(
onRefresh: controller.refresh,
child: ListView.builder(
controller: scrollCtrl,
itemCount: itemList.length + 1,
itemBuilder: (context, index) {
if (index < itemList.length) {
return ListTile(title: Text(itemList[index]));
}
final st = controller.loader.watch(context);
return switch (st) {
AsyncLoading() => const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
),
AsyncError(: final e, _) => Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Column(
children: [
Text("Load more failed: $e"),
const SizedBox(height: 8),
ElevatedButton(
onPressed: controller.loadNextPage,
child: const Text("Retry"),
),
],
),
),
),
AsyncData() => Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Text(
controller.hasMore.watch(context)
? ""
: "--- No more data ---",
),
),
),
_ => const SizedBox.shrink(),
};
},
),
),
);
}
}
五、异步信号 vs Riverpod / FutureProvider / StreamProvider
Riverpod 是 Flutter 社区非常流行的状态管理和依赖注入框架。它的 FutureProvider 和 StreamProvider 提供了与 signals 类似的功能。我们用同样的需求来对比一下。
Riverpod 实现思路:
scala
// 1. 定义一个 Provider 来获取一页数据
final itemsProvider = FutureProvider.family<List<String>, int>((ref, page) async {
final apiService = ref.watch(apiServiceProvider);
return apiService.fetchItems(page);
});
// 2. 使用一个 StateNotifier 来管理列表状态
class PaginatedNotifier extends StateNotifier<PaginatedState> {
PaginatedNotifier(this.ref) : super(PaginatedState.initial()) {
fetchFirstPage();
}
final Ref ref;
Future<void> fetchNextPage() async {
// ...管理 state.isLoading, state.page, 追加数据等...
// 读取 FutureProvider
final result = await ref.read(itemsProvider(state.page).future);
// ...更新状态...
}
// ... refresh 逻辑 ...
}
final paginatedNotifierProvider = StateNotifierProvider<PaginatedNotifier, PaginatedState>((ref) {
return PaginatedNotifier(ref);
});
// 3. 在 UI 中监听 Notifier
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(paginatedNotifierProvider);
final notifier = ref.read(paginatedNotifierProvider.notifier);
// ... 根据 state 构建 UI ...
}
对比分析:
特性 | Signals | Riverpod |
---|---|---|
代码量/模板 | 更少。信号的定义和组合非常直接,无需额外的 Provider 定义。 | 更多。需要定义 Provider、StateNotifier、State 类,结构更严谨但模板代码也更多。 |
可读性 | 高。逻辑通常集中在 Controller 类中,signal, computed 的响应式流易于理解。 | 中等。状态分散在多个 Provider 中(Notifier 管理列表,FutureProvider 管理请求),需要理解 Provider 之间的依赖关系。 |
状态判断 | 优雅。AsyncState.when 提供了编译时安全的状态匹配。 | 优雅。AsyncValue.when 提供同样强大的功能。 |
刷新/重试机制 | 内置。.refresh(), .reload() 方法简单易用。 | 内置。ref.invalidate() 或 ref.refresh(),功能强大。 |
组合能力 | 强。computed 可以轻松组合任何信号,创建派生状态。 | 强。一个 Provider 可以依赖其他多个 Provider,构建复杂的状态图。 |
性能 | 优秀。细粒度更新,只有监听特定信号的 Widget 会重建。 | 优秀。同样具备细粒度更新的能力。 |
优势与弱点 | 优势:上手快,模板代码少,心智负担低,对于从其他响应式框架(如 Solid.js)过来的开发者非常友好。弱点:不自带依赖注入(DI)体系;复杂的取消和并发控制(如 debounce, throttle)需要自己实现。 | 优势:强大的 DI 体系,完善的缓存策略(autoDispose),丰富的 Provider 类型,生态成熟。弱点:学习曲线稍陡峭,概念(family, autoDispose)较多,代码量相对较大。 |
总的来说,signals 在纯粹的异步 状态管理 层面提供了更轻量、更直接的解决方案。而 Riverpod 则是一个更全面的 应用架构 框架,集成了状态管理、依赖注入和缓存。
六、 注意事项
- 组织异步逻辑 :始终将 API 调用、数据库操作等逻辑封装在单独的 Repository 或 Service 类中。futureSignal 内部应该调用 myRepository.fetchData() ,而不是直接发起 http.get。
- 避免重复请求 :futureSignal 默认会缓存其 Future 的实例。除非你调用 .refresh() 或 .reload() ,否则它不会重新执行。对于需要参数的请求,可以结合 computed 来创建动态的 futureSignal。
- 缓存与持久化 :signals 本身只处理内存中的状态。如果需要跨应用会话的缓存或持久化,你需要结合 shared_preferences 或 hive 等库,并在 effect 中监听信号变化以写入存储。
- 可测试性 :由于我们将逻辑封装在 PaginatedController 这样的普通 Dart 类中,测试变得非常简单。你可以轻松地 mock ApiService,然后调用控制器的方法,并断言其内部信号的值是否符合预期,完全无需 Flutter 环境。
- 与其他方案组合 :signals 是一个纯粹的状态管理库,它可以与任何依赖注入框架(如 get_it )或架构模式(如 BLoC)和平共处。在一个大型应用中,你可以用 get_it 管理全局服务和 Repository ,同时在 Feature 层面或 Widget 层面使用 signals 来管理局部和UI相关的异步状态。
总结
Flutter signals 为异步状态管理提供了一套声明式、简洁且强大的工具。通过 AsyncState 、futureSignal 和 streamSignal,它将异步操作的复杂生命周期(加载、数据、错误)封装成一个易于消费的响应式状态。
这不仅极大地减少了处理异步逻辑所需的模板代码,还通过内置的竞态条件处理和刷新机制,解决了许多传统方法中的常见痛点。无论是简单的 API 调用,还是复杂的分页加载列表,signals 都能让你写出更清晰、更健壮、更易于维护的代码。