Flutter 使用 Provider 管理状态的实践记录
date
May 24, 2021
slug
practice-record-using-provider-in-flutter-to-manage-state
status
Published
summary
通过全局的 Provider 管理文章的数据,当发生更新时,直接 Rebuild 对应的 Widget 即可
tags
Flutter
type
Post
一、背景二、示例一:文章流 & 交互2.1 建立 Model2.2 列表构建2.2.1 注册 Model2.2.2 如何实现列表构建(数据获取和消费)2.3 已读和收藏2.4 Hot Reload 带来的一些问题三、示例二:Tabbar 状态更新3.1 定义 Model 属性3.2 状态获取和更新3.3 切换 Tab 时的一些影响
一、背景
在做一个自用的 TinyTiny RSS 阅读器,其中将文章列表部分拆分成了三个模块以减少嵌套。
同时,针对于单篇文章需要提供点击文章和修改标题颜色、标记已读和未读、收藏和取消收藏的功能;因此涉及到子组件修改父组件的参数,所以需要在父组件提供一个方法用于修改,大致代码如下:
- UnreadPage:页面父组件
// 接收子组件回调,更新未读列表 void onChanged(List<ArticlesInFeed> value) { setState(() { this.articlesInFeeds = value; }); } Widget _getArticleList(int index) { // 生成 Feed 流 return FeedItem( feedId: this.articlesInFeeds[index].id, feedIcon: this.articlesInFeeds[index].feedIcon, feedTitle: this.articlesInFeeds[index].feedTitle, feedArticles: this.articlesInFeeds[index].feedArticles, articlesInfeeds: this.articlesInFeeds, callBack: (value) => onChanged(value), ); } @override Widget build(BuildContext context) { return ListView.builder( physics: AlwaysScrollableScrollPhysics(), itemCount: this.unreadArticleList.length, itemCount: this.articlesInFeeds.length, itemBuilder: (context, index) { return this._getArticleList(index); }; }
- FeedItem:Feed 流子组件
// 接收子组件回调,更新已读和收藏状态 void onChanged(Map<String, int> value) { setState(() { article['isRead'] = value["isRead"]; article['isStar'] = value["isStar"]; TinyTinyRss().markRead([articleId], isRead: value["isRead"]); TinyTinyRss().markStar(articleId, value["isStar"]); }); } @override Widget build(BuildContext context) { AppDatabase database = Provider.of<AppDatabase>(context, listen: false); return ArticleItem( id: article.id, title: article.title, isRead: article.isRead, isStar: article.isStar, description: article.description, flavorImage: article.flavorImage, publishTime: Tool().timestampToDate(article.publishTime), callBack: (Map<String, int> value) => onChanged(value), ); }
- ArticleItem:article 流子组件
可以看到这样的结构比较繁琐,而且必须要使用 StatefulWidget 用于更新状态;
因此,想到通过全局的
Provider
管理文章的数据,当发生更新时,直接 Rebuild
对应的 Widget
即可。二、示例一:文章流 & 交互
2.1 建立 Model
import 'package:flutter/material.dart'; class ArticleModel with ChangeNotifier { List<ArticlesInFeed> initData = []; List<ArticlesInFeed> get getData => initData; int get total => initData.length; Future<void> update({bool isLaunch = false}) async { // 修改数据 notifyListeners(); } setReadStatus(…) { // 修改数据 notifyListeners(); } setStarStatus(…) { // 修改数据 notifyListeners(); } }
该 Model 主要提供更新文章数据,更新阅读和星标状态等功能。
2.2 列表构建
这部分主要是如何去使用 Provider 提供的数据:
2.2.1 注册 Model
import 'package:provider/provider.dart'; import 'Model/ArticleModel.dart'; void main() async { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (context) { final db = Provider.of<AppDatabase>(context, listen: false); return ArticleModel(db); }, child: Child(), ); } }
这个是正常的注册方式,但是有时候会有很多个 Model 需要处理,因此可以处理成这样:
import 'package:provider/provider.dart'; import 'Model/ArticleModel.dart'; … void main() async { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiProvider( providers: [ // 数据库 Provider<AppDatabase>(create: (_) => constructDb()), // 底栏 Tab ChangeNotifierProvider<BottomNavyModel>( create: (_) => BottomNavyModel()), ], child: ChangeNotifierProvider( create: (context) { final db = Provider.of<AppDatabase>(context, listen: false); // 文章数据,同时传入数据库实例去构造函数 return ArticleModel(db); }, child: Child(), ); } }
2.2.2 如何实现列表构建(数据获取和消费)
状态获取有三种方式:
- Provider.of 方法:
Provider.of<Model>(context, listen: false)
- Consumer Widget:
Consumer(builder: (context, Model provider, _) => Text('Value: ${provider.data}'))
- Selector Widget:
Selector<Model, Model>( selector: (context, Model provider) => provider, builder: (context, Model provider, child) { await provider.update(); return Child(provider.data); } )
每一次数据更新后,对应使用相关数据的 Widget 都会触发 Rebuild,而上述三种中后两种都可以实现精确地控制刷新粒度;
而 Selector 尤其适用于列表构建、针对单条数据单独触发 UI 刷新和管理是否需要刷新 UI,示例:
ListView.builder( physics: BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics()), itemCount: provider.total, itemBuilder: (context, index) { return Selector<ArticleModel, ArticlesInFeed>( selector: (context, provider) => provider.getData[index], shouldRebuild: (prev, next) => false, builder: (context, data, child) { return FeedItem( index, data, provider, ); }, ); }, )
其中,当
shouldRebuild
设置为 false
,则代表就算状态更新了该列表项也不会触发 UI 刷新;shouldRebuild
默认为 prev ≠ next
。另外,可以进一步优化列表,在外面包一层
Selector
,并且设置 shouldRebuild: (prev, next) => false
,这样就禁止了因为状态更新导致整个列表重新刷新 UI 的可能。Selector<ArticleModel, ArticleModel>( selector: (context, provider) => provider, shouldRebuild: (prev, next) => false, builder: (context, provider, child) { return ListView.builder( physics: BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics()), itemCount: provider.total, itemBuilder: (context, index) { return Selector<ArticleModel, ArticlesInFeed>( selector: (context, provider) => provider.getData[index], builder: (context, data, child) { return FeedItem( index, data, provider, ); }, ); }, ) } )
2.3 已读和收藏
针对单篇文章的已读和收藏状态刷新也是一样,通过 Selector 更新:
Selector<ArticleModel, Article>( selector: (context, provider) => provider.getData[this.feedIndex].feedArticles[this.articleIndex], builder: (context, data, child) { bool hasImage = data.flavorImage.isNotEmpty; return Slidable( actionPane: SlidableDrawerActionPane(), actionExtentRatio: 0.25, child: this._getArticleItem(context, data, hasImage), actions: this._getArticleSlideItem(data.isRead, data.isStar), ); }, )
2.4 Hot Reload 带来的一些问题
另外需要注意的一点是,有时候 Model 注册使用了
ChangeNotifierProvider.value
会导致调试时热更新让状态数据丢失。解决方案是使用下述方式注册
ChangeNotifierProvider( builder: (_) => Model(), ) //例: ChangeNotifierProvider( create: (context) { return Model(); }, child: Child(), )
三、示例二:Tabbar 状态更新
3.1 定义 Model 属性
import 'package:flutter/material.dart'; class BottomNavyModel with ChangeNotifier { int _currentIndex = 0; int get currentIndex => _currentIndex; set currentIndex(int index) { _currentIndex = index; notifyListeners(); } }
3.2 状态获取和更新
class MyApp extends StatelessWidget { final List<Widget> _pages = [ Page1(), Page2(), ]; final PageController _pageController = PageController(initialPage: 0); List<BottomNavigationBarItem> get getBottomTabbarItemList => [ BottomNavigationBarItem( icon: Icon(Icons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(Icons.mail), label: 'Messages', ), BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile') ]; @override Widget build(BuildContext context) { final router = FluroRouter(); //给routers.dart里面的传递FluroRouter传递router实例对象 Routes.configureRoutes(router); //给Application的router赋值router实例对象 Application.router = router; return MultiProvider( providers: [ Provider<AppDatabase>(create: (_) => constructDb()), ChangeNotifierProvider<BottomNavyModel>( create: (_) => BottomNavyModel()), ], child: ChangeNotifierProvider( create: (context) { final db = Provider.of<AppDatabase>(context, listen: false); return ArticleModel(db); }, child: MaterialApp( home: Selector<BottomNavyModel, BottomNavyModel>( selector: (context, provider) => provider, builder: (context, provider, child) { return Scaffold( body: PageView( controller: this._pageController, children: this._pages, onPageChanged: (int index) { provider.currentIndex = index; }, ), backgroundColor: Colors.white, bottomNavigationBar: Selector<BottomNavyModel, int>( selector: (context, provider) => provider.currentIndex, builder: (context, index, child) { return BottomNavigationBar( onTap: (int selectedIndex) { provider.currentIndex = selectedIndex; this._pageController.jumpToPage(selectedIndex); }, currentIndex: index, items: this.getBottomTabbarItemList, ); }, ), ); }, ), ), ), ); } }
3.3 切换 Tab 时的一些影响
目前默认情况下切换 Tab 会导致前一页的状态丢失,因此需要使用一些方式进行处理:
在需要保持状态的页面添加
with AutomaticKeepAliveClientMixin
,同时添加bool _wantKeepAlive = true
class UnreadPage extends StatefulWidget { @override _UnreadPageState createState() => _UnreadPageState(); } class _UnreadPageState extends State<UnreadPage> with AutomaticKeepAliveClientMixin { bool _wantKeepAlive = true; }
这样,从该页面切换出去,就不会丢失状态了。
但是,与此同时还带来一个问题,有时候我们需要更新页面的状态用以展示新的数据,如果一直强制保持,会导致页面无法展示出新的数据。
针对这个问题,可以通过定义
wantKeepAlive
,并通过方法updateKeepAlive
来修改当前页面是否需要保持状态。class UnreadPage extends StatefulWidget { @override _UnreadPageState createState() => _UnreadPageState(); } class _UnreadPageState extends State<UnreadPage> with AutomaticKeepAliveClientMixin { bool _wantKeepAlive = true; @override bool get wantKeepAlive => _wantKeepAlive; void exampleUpdateKeepAliveState() { setState(() { this._wantKeepAlive = false; this.updateKeepAlive(); }); //这里可以做数据更新处理 setState(() { this._wantKeepAlive = true; this.updateKeepAlive(); }); } }