
架构分层
项目的状态管理分为三层:
MultiProvider(应用根节点)
├── Provider<AtomGitApiClient> --- 全局服务
├── ChangeNotifierProvider<AuthProvider> --- 全局状态
│
└── 各页面(按需创建)
├── ChangeNotifierProvider<RepoDetailProvider>
├── ChangeNotifierProvider<CodeProvider>
├── ChangeNotifierProvider<IssueProvider>
└── ...
全局 Provider
在 MaterialApp 之上通过 MultiProvider 注入:
dart
class AtomGitApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<AtomGitApiClient>(
create: (_) => AtomGitApiClient(),
),
ChangeNotifierProvider<AuthProvider>(
create: (_) => AuthProvider(
apiClient: /* 引用上面的 ApiClient */,
)..tryRestoreSession(),
),
],
child: MaterialApp(/* ... */),
);
}
}
| Provider | 类型 | 生命周期 | 用途 |
|---|---|---|---|
AtomGitApiClient |
Provider(不变) |
App 级别 | HTTP 客户端,被所有页面注入 |
AuthProvider |
ChangeNotifierProvider |
App 级别 | 登录状态,驱动 Tab UI 切换 |
Provider<T> 用于不变的服务对象。ChangeNotifierProvider<T> 用于会变化、需要通知 UI 的状态。
页面级 Provider
每个详情页在 build 中创建自己的 Provider:
dart
class RepoDetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final args =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return ChangeNotifierProvider(
create: (_) =>
RepoDetailProvider(context.read<AtomGitApiClient>())
..load(args['owner'], args['name']),
child: _RepoDetailBody(/* ... */),
);
}
}
关键模式:create 中完成两件事:
- 通过
context.read<AtomGitApiClient>()获取全局 ApiClient - 通过
..load()级联语法立即触发数据加载
Provider 的生命周期与页面绑定:页面被 Navigator.pop 移除时,Provider 自动 dispose。
读取 Provider 的三种方式
context.read() --- 一次性读取
dart
// 在 create/onTap 等回调中,不需要监听
final apiClient = context.read<AtomGitApiClient>();
provider.loadMore();
不会导致重建,适合事件处理函数内使用。
context.watch() --- 持续监听
dart
// 在 build 方法中,需要随状态变化重建
final isLoggedIn = context.watch<AuthProvider>().isLoggedIn;
每当 Provider 调用 notifyListeners(),Widget 就会重建。适合 build 方法内使用。
Consumer --- 局部监听
dart
ChangeNotifierProvider.value(
value: _userProvider!,
child: Consumer<UserProvider>(
builder: (context, provider, _) {
return Text('仓库: ${provider.user?.publicRepos}');
},
),
);
只有 Consumer 包裹的部分会重建,外层 Widget 不受影响。适合局部优化。
标准 Provider 模板
所有 Provider 遵循统一的加载模式:
dart
class SomeProvider extends ChangeNotifier {
final AtomGitApiClient _apiClient;
List<Item> _items = [];
bool _isLoading = false;
String? _error;
bool _hasMore = false;
int _page = 1;
// Getters
List<Item> get items => List.unmodifiable(_items);
bool get isLoading => _isLoading;
String? get error => _error;
bool get hasMore => _hasMore;
Future<void> load() async {
_page = 1;
_isLoading = true;
_error = null;
notifyListeners(); // ① 通知:开始加载
try {
final response = await _apiClient.get(/* ... */);
_items = /* parse response */;
_hasMore = _items.length >= 30;
} on ApiException catch (e) {
_error = e.message; // ② 区分 API 异常
} catch (e) {
_error = '加载失败'; // ③ 兜底错误
} finally {
_isLoading = false;
notifyListeners(); // ④ 通知:加载完成
}
}
}
四次 notifyListeners() 的节奏:
- 加载前:设置 loading 状态,UI 展示加载指示器
- 加载后(finally):清除 loading,UI 展示数据/错误
UI 状态模式
每个页面的 buildBody 方法遵循三态逻辑:
dart
Widget _buildBody(SomeProvider provider) {
// 优先级 1:错误(且无缓存数据)
if (provider.error != null && provider.items.isEmpty) {
return ErrorRetryWidget(
message: provider.error!,
onRetry: () => provider.load(),
);
}
// 优先级 2:加载中
if (provider.isLoading && provider.items.isEmpty) {
return const LoadingIndicator(message: '加载中...');
}
// 优先级 3:空结果
if (provider.items.isEmpty) {
return const Center(child: Text('暂无数据'));
}
// 优先级 4:正常数据
return ListView.builder(/* ... */);
}
错误优先于加载中优先于空结果优先于正常数据 ------ 这是判断链的顺序。
AuthProvider --- 全局状态驱动
AuthProvider 是唯一贯穿全应用的状态:
dart
class AuthProvider extends ChangeNotifier {
final AtomGitApiClient _apiClient;
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
Future<void> tryRestoreSession() async {
final token = await LocalStorage.instance.read<String>('access_token');
if (token != null && token.isNotEmpty) {
_apiClient.setAccessToken(token);
_isLoggedIn = true;
notifyListeners();
}
}
Future<void> setTokenFromManualInput(String token) async {
_apiClient.setAccessToken(token);
await LocalStorage.instance.write('access_token', token);
_isLoggedIn = true;
notifyListeners();
}
Future<void> logout() async {
_apiClient.setAccessToken(null);
await LocalStorage.instance.delete('access_token');
_isLoggedIn = false;
notifyListeners();
}
}
登录/登出后的连锁反应依赖 Provider 的广播机制:
AuthProvider.notifyListeners()
→ 所有 context.watch<AuthProvider>() 的 Widget 重建
→ MainShell 底部导航切换显示
→ ProfileTab 创建/销毁 UserProvider
→ HomeTab 切换数据源
→ NotificationsTab 切换登录引导/占位
不需要手动通知各个组件,Provider 自动处理依赖传播。
changeNotifierProvider.value vs create
dart
// 标准用法:Provider 由 ChangeNotifierProvider 创建和 dispose
ChangeNotifierProvider(
create: (_) => SomeProvider(),
child: _Body(),
)
// value 用法:Provider 已存在(手动管理生命周期)
ChangeNotifierProvider.value(
value: existingProvider,
child: Consumer<SomeProvider>(/* ... */),
)
ProfileTab 使用 .value 的原因是它手动管理 UserProvider 的创建/销毁(跟随登录状态),不能交给 create 自动处理。
避免嵌套地狱
通过 Provider 的架构,UI 代码不直接持有数据加载逻辑:
dart
// 不这样写:UI 中直接调 API
class _BodyState extends State<_Body> {
List<Repository> _repos = [];
bool _loading = false;
Future<void> _load() async {
setState(() => _loading = true);
final resp = await http.get(/* ... */);
setState(() { _repos = /* ... */; _loading = false; });
}
}
// 而是这样写:Provider 封装数据逻辑
class _BodyState extends State<_Body> {
@override
Widget build(BuildContext context) {
final provider = context.watch<RepoDetailProvider>();
if (provider.isLoading) return LoadingIndicator();
return ListView(/* provider.repositories */);
}
}
Provider 将异步加载、错误处理、分页状态从 Widget 的 State 中剥离,Widget 只关心"当前应该展示什么"。