最近在学习Flutter中的状态管理用到provider时,原本以为按着文档的简单示例写一点问题都没有,结果惨遭各种翻车,整个屏幕宛如一片红海,因此,写下该篇文章,记录学习。那现在就开始吧。
这里先使用新建Flutter项目的计数器来演示。
导入依赖 在项目中的pubspec.yaml中导入provider最新的版本依赖,目前最新版本为4.3.2。 dependencies: flutter: sdk: flutter provider: ^4.3.2 # 导入最新版本 构建Model 新建CountModel类并继承于ChangeNotifier,定义成员变量count用于存储当前计数值,定义方法increment使count值自增1。 import 'package:flutter/material.dart'; class CountModel extends ChangeNotifier{ int count = 0; void increment(){ count++; notifyListeners(); // 通知数据更新了。 } } 使用Provider 首先我们看看文档推荐的使用方式: // DO create a new object inside create. Provider( create: (_) => MyModel(), child: ... )我们按照这种方式使用,看起来超级简单,实际上处处劝退。
这里省略计数器示例的其他代码,仅展示build方法代码。
@override Widget build(BuildContext context) { return Provider( create: (_) => CountModel(), child: Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Text( '${context.watch<CountModel>().count}', // 观察值的变化 style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: context.read<CountModel>().increment, // 点击事件 tooltip: 'Increment', child: Icon(Icons.add), ), ), ); }然后直接运行。不出意外的话直接报错,效果如下: 出错了不要慌,仔细一看,哇塞,竟然还给出了示例代码,这么好的吗?通过仔细比对发现:Provider没有持有泛型,不使用child参数而是使用builder参数,代码如下:
@override Widget build(BuildContext context) { return Provider<CountModel>( create: (_) => CountModel(), builder: (context,child){ return Scaffold(...); }, );然后下意识ctrl+s,发现还是报错,但是报错信息变了,信息如下:
Tried to use Provider with a subtype of Listenable/Stream (CountModel).报错中还有提示:
such as : - ListenableProvider - ChangeNotierProvider - ValueListenableProvider - StreamProvider然后根据提示将代码修改为如下:
@override Widget build(BuildContext context) { return ListenableProvider<CountModel>( create: (_) => CountModel(), builder: (context,child){ return Scaffold(...); }, ); }再次运行,发现仍然报错,信息如下: 哈哈哈,是不是顿时被劝退了。那我们从第一次报错开始看。
解决错误 第一次的报错信息: Error: Could not find the correct Provider<CountModel> above this MyHomePage Widget // 错误:在此MyHomePage小部件上方找不到正确的Provider <CountModel>在MyHomePage上方找不到正确的Provider?那么就应该在上方放置正确的Provider就行了。查看代码发现MyHomePage对象是在MaterialApp下创建的,那么就应该在MaterialApp下创建正确的Provider。
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: ListenableProvider<CountModel>( // 使用了watch进行观察 create: (_) => CountModel(), child: MyHomePage(title: 'Flutter Demo Home Page'), ), ); } } class _MyHomePageState extends State<MyHomePage> { CountModel model; // 创建model @override Widget build(BuildContext context) { model = Provider.of<CountModel>(context); // 获取model return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Text( '${context.watch<CountModel>().count}', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: model.increment, tooltip: 'Increment', child: Icon(Icons.add), ), ); } } 注意事项 报错:Tried to use context.read<CountModel> inside either a build method or the update callback of a provider. model获取方式出错,不能使用read()获取model,而是应该使用Provider.of()进行获取。报错:Tried to use Provider with a subtype of Listenable/Stream (CountModel). Provider类型出错,不使用Provider,建议使用ListenableProvider。上面这种方式虽然实现了数据改变后视图自动刷新,但是这种刷新是全局刷新,每一次刷新都会重新执行一次build方法,这样效率不太高而且比较浪费资源,因此在比较复杂的界面可以采用Selector,它可以实现局部刷新。
Selector参数 class Selector<A, S> extends Selector0<S> { /// {@macro provider.selector} Selector({ Key key, @required ValueWidgetBuilder<S> builder, @required S Function(BuildContext, A) selector, ShouldRebuild<S> shouldRebuild, Widget child, }) : assert(selector != null), super( key: key, shouldRebuild: shouldRebuild, builder: builder, selector: (context) => selector(context, Provider.of(context)), child: child, ); }可以看到Selector持有两个泛型A和S,构造方法中有两个必要参数builder和selector。selector参数是一个参数为BuildContext和A,返回值为S的方法。再来看看builder。
typedef ValueWidgetBuilder<T> = Widget Function(BuildContext context, T value, Widget child);builder其实就是一个参数为BuilderContext、T以及Widget返回值为Widget的方法。因此,可以得知,泛型A就是model的类型,泛型S就是被观察的值的类型。参数含义知道了,那么问题就简单了,原有示例代码可以改写成如下:
class _MyHomePageState extends State<MyHomePage> { CountModel model; @override Widget build(BuildContext context) { model = Provider.of<CountModel>(context); return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Selector<CountModel,int>( // 实现局部刷新 builder: (context,count,child) => Text( '$count', style: Theme.of(context).textTheme.headline4, ), selector: (context,model) => model.count, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: model.increment, tooltip: 'Increment', child: Icon(Icons.add), ), ); } }使用Selector后,被观察的值改变后并不是每次都会调用build方法,只是会刷新局部视图,节省了资源。
