AtomGit Flutter鸿蒙客户端:Provider状态管理
架构分层
项目的状态管理分为三层:
MultiProvider(应用根节点) ├── Provider<AtomGitApiClient> — 全局服务 ├── ChangeNotifierProvider<AuthProvider> — 全局状态 │ └── 各页面(按需创建) ├── ChangeNotifierProvider<RepoDetailProvider> ├── ChangeNotifierProvider<CodeProvider> ├── ChangeNotifierProvider<IssueProvider> └── ...全局 Provider
在MaterialApp之上通过MultiProvider注入:
classAtomGitAppextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnMultiProvider(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:
classRepoDetailScreenextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){finalargs=ModalRoute.of(context)!.settings.argumentsasMap<String,dynamic>;returnChangeNotifierProvider(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() — 一次性读取
// 在 create/onTap 等回调中,不需要监听finalapiClient=context.read<AtomGitApiClient>();provider.loadMore();不会导致重建,适合事件处理函数内使用。
context.watch() — 持续监听
// 在 build 方法中,需要随状态变化重建finalisLoggedIn=context.watch<AuthProvider>().isLoggedIn;每当 Provider 调用notifyListeners(),Widget 就会重建。适合 build 方法内使用。
Consumer — 局部监听
ChangeNotifierProvider.value(value:_userProvider!,child:Consumer<UserProvider>(builder:(context,provider,_){returnText('仓库:${provider.user?.publicRepos}');},),);只有 Consumer 包裹的部分会重建,外层 Widget 不受影响。适合局部优化。
标准 Provider 模板
所有 Provider 遵循统一的加载模式:
classSomeProviderextendsChangeNotifier{finalAtomGitApiClient_apiClient;List<Item>_items=[];bool _isLoading=false;String?_error;bool _hasMore=false;int _page=1;// GettersList<Item>getitems=>List.unmodifiable(_items);boolgetisLoading=>_isLoading;String?geterror=>_error;boolgethasMore=>_hasMore;Future<void>load()async{_page=1;_isLoading=true;_error=null;notifyListeners();// ① 通知:开始加载try{finalresponse=await_apiClient.get(/* ... */);_items=/* parse response */;_hasMore=_items.length>=30;}onApiExceptioncatch(e){_error=e.message;// ② 区分 API 异常}catch(e){_error='加载失败';// ③ 兜底错误}finally{_isLoading=false;notifyListeners();// ④ 通知:加载完成}}}四次notifyListeners()的节奏:
- 加载前:设置 loading 状态,UI 展示加载指示器
- 加载后(finally):清除 loading,UI 展示数据/错误
UI 状态模式
每个页面的 buildBody 方法遵循三态逻辑:
Widget_buildBody(SomeProviderprovider){// 优先级 1:错误(且无缓存数据)if(provider.error!=null&&provider.items.isEmpty){returnErrorRetryWidget(message:provider.error!,onRetry:()=>provider.load(),);}// 优先级 2:加载中if(provider.isLoading&&provider.items.isEmpty){returnconstLoadingIndicator(message:'加载中...');}// 优先级 3:空结果if(provider.items.isEmpty){returnconstCenter(child:Text('暂无数据'));}// 优先级 4:正常数据returnListView.builder(/* ... */);}错误优先于加载中优先于空结果优先于正常数据 —— 这是判断链的顺序。
AuthProvider — 全局状态驱动
AuthProvider 是唯一贯穿全应用的状态:
classAuthProviderextendsChangeNotifier{finalAtomGitApiClient_apiClient;bool _isLoggedIn=false;boolgetisLoggedIn=>_isLoggedIn;Future<void>tryRestoreSession()async{finaltoken=awaitLocalStorage.instance.read<String>('access_token');if(token!=null&&token.isNotEmpty){_apiClient.setAccessToken(token);_isLoggedIn=true;notifyListeners();}}Future<void>setTokenFromManualInput(Stringtoken)async{_apiClient.setAccessToken(token);awaitLocalStorage.instance.write('access_token',token);_isLoggedIn=true;notifyListeners();}Future<void>logout()async{_apiClient.setAccessToken(null);awaitLocalStorage.instance.delete('access_token');_isLoggedIn=false;notifyListeners();}}登录/登出后的连锁反应依赖 Provider 的广播机制:
AuthProvider.notifyListeners() → 所有 context.watch<AuthProvider>() 的 Widget 重建 → MainShell 底部导航切换显示 → ProfileTab 创建/销毁 UserProvider → HomeTab 切换数据源 → NotificationsTab 切换登录引导/占位不需要手动通知各个组件,Provider 自动处理依赖传播。
changeNotifierProvider.value vs create
// 标准用法:Provider 由 ChangeNotifierProvider 创建和 disposeChangeNotifierProvider(create:(_)=>SomeProvider(),child:_Body(),)// value 用法:Provider 已存在(手动管理生命周期)ChangeNotifierProvider.value(value:existingProvider,child:Consumer<SomeProvider>(/* ... */),)ProfileTab 使用.value的原因是它手动管理 UserProvider 的创建/销毁(跟随登录状态),不能交给create自动处理。
避免嵌套地狱
通过 Provider 的架构,UI 代码不直接持有数据加载逻辑:
// 不这样写:UI 中直接调 APIclass_BodyStateextendsState<_Body>{List<Repository>_repos=[];bool _loading=false;Future<void>_load()async{setState(()=>_loading=true);finalresp=awaithttp.get(/* ... */);setState((){_repos=/* ... */;_loading=false;});}}// 而是这样写:Provider 封装数据逻辑class_BodyStateextendsState<_Body>{@overrideWidgetbuild(BuildContextcontext){finalprovider=context.watch<RepoDetailProvider>();if(provider.isLoading)returnLoadingIndicator();returnListView(/* provider.repositories */);}}Provider 将异步加载、错误处理、分页状态从 Widget 的 State 中剥离,Widget 只关心"当前应该展示什么"。
