Flutter Provider状态管理-Consumer
为什么要使用状态管理:
我们都知道在flutter中要改变页面非常简单,只需要setState就可以了,但是调用这个方法的代码是极其昂贵的,会导致我们整个页面的重绘,简单的“计数器”就不说了,即便是一直setState我们也不太会感知页面的卡顿,但是如果我们把页面换成“淘宝”,“一东”这种复杂的页面呢?你一个按钮文字的变化都会导致整个页面的重绘,代码是不是太大了。。。
话不多说,我们来看看Provider的使用:
1.引包
dependencies: provider: ^3.1.0+1
多说一句:强烈建议使用3.1.0以后的版本,至于原因嘛,后续有时间会提到
2.采用最简单的计数器代码来整合provider
一共有三个文件
2.1 main.dart 这个不用多说,程序的入口
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
///这里用到了MultiProvider,针对多个Provider的使用场景
///在这里我们初始化了CounterProvider并且指定了child为MaterialApp
return MultiProvider(
providers: [ChangeNotifierProvider(builder: (_) => CounterProvider())],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
routes: {
Routes.INDEX_PAGE: (context) => IndexPage(),
Routes.STOCK_LIST_PAGE: (context) => StockListPage(),
Routes.QRCODE_SCAN_PAGE: (context) => QrCodeScanPage(),
Routes.LOGIN: (context) => LoginPage(),
Routes.SPLASH: (context) => SplashPage(),
Routes.MY_APP: (context) => MyPage()
},
initialRoute: Routes.MY_APP,
),
);
}
}
在程序的入口我们用MultiProvider包裹了一层,而且初始化了CounterProvider,把child指定为MaterialApp。
注:不建议在程序入口初始化Provider,这里只是为了演示方便这么做,实际项目中要是都在程序入口初始化可能会导致内存急剧增加,除非是共享一些全局的状态,例如app日夜间模式切换,中英文切换等。。。。
2.2 CounterProvider
class CounterProvider with ChangeNotifier {
int _count = 0;
int get value => _count;
void increment() {
_count++;
notifyListeners();
}
}
这个类做了三件事:
- 继承自ChangeNotifier
- 初始化count为0(这也是我们要提供出去的数据),并提供一个get方法
- 声明一个increment函数来改变count的值,并且每次改变都会触发notifyListeners(),这个方法的作用是通知CounterProvider的宿主我的值已经改变了
2.3 my_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_provider.dart';
class MyPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyPageState();
}
class MyPageState extends State<MyPage> {
@override
Widget build(BuildContext context) {
//获取CounterProvider
CounterProvider counterProvider = Provider.of<CounterProvider>(context);
print('页面重绘了。。。。。。。。。。。');
return Scaffold(
appBar: AppBar(
title: Text('my page'),
),
body: Center(
child: Text(
//获取数据
'value: ${counterProvider.value}',
style: TextStyle(fontSize: 20),
)),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.navigation),
onPressed: () {
//调用increment方法改变数据
counterProvider.increment();
}),
);
}
}
啊哈哈,终于到UI了,看到了我们熟悉的Widget, 代码很简单,主要是三步:
- 在build方法里获取到在程序入口(main.dart)初始化的CountProvider
- 点击按钮调用increment方法,改变了count的值
- 在text中获取count的值
由于Provider也是数据驱动UI,所以我们只需要把值填上去,UI就能自动刷新。经过测试发现确实达到了我们预期的效果,每点击一次按钮text的数值都会加一。
但不要忘了我们一开始说的:我们用状态管理最重要的是解决整个页面重绘的问题!
在上面的demo中我们只希望Text这个widget重绘,毕竟它的值是改变了,但是按钮以及appbar我们是不希望重绘的,所以我们加了一句print语句,如果我每次点击按钮都会打印“页面重绘了。。。。。。”那就是一件很蛋疼的事,那事实是怎样呢?
我们在代码中是采用Provider.of来获取CounterProvider,这中获取方式确实会引起整个页面的重绘,至于原因不在本章讨论范围,以后有时间再说。 那么Provider到底能不能实现“局部刷新”呢? 当然是可以的,不然这个框架真的没啥用了。下面我们再来认识一位重量级嘉宾:
3.Cosumer
这一节我们沿用计数器的代码,对其进行改造。之前我们提到了在程序入口初始化Provider是很不规范的,所以我们改成在页面级别初始化,并结合Consumer来使用。所以我们就剩下两个文件了,CountProvider和MyPage
3.1 CountProvider
和之前的一模一样,没改!!!!
3.2 MyPage
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_provider.dart';
class MyPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyPageState();
}
class MyPageState extends State<MyPage> {
///初始化CounterProvider
CounterProvider _counterProvider = new CounterProvider();
@override
Widget build(BuildContext context) {
print('页面重绘了。。。。。。。。。。。');
//整个页面使用ChangeNotifier来包裹
return ChangeNotifierProvider(
builder: (context) => _counterProvider,
child:
//child里面的内容不会因为数据的改变而重绘
Scaffold(
appBar: AppBar(
title: Text('my page'),
),
body: Center(child:
//使用Cousumer来获取Provider
Consumer(builder: (BuildContext context,
CounterProvider counterProvider, Widget child) {
print('Text重绘了。。。。。。');
return Text(
//获取数据
'value: ${counterProvider.value}',
style: TextStyle(fontSize: 20),
);
})),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.navigation),
onPressed: () {
//调用increment方法改变数据
_counterProvider.increment();
}),
),
);
}
}
稍微复杂了点,请耐心看下去
- 我们把CounterProvider的初始化提到了MyPageState
- 整个页面用ChangeNotifierProvider来包裹,并指定数据源是CounterProvider
- 摒弃之前获取Provider的方式,使用Consumer来获取
先说结论:用上面的代码去运行并不会导致整个页面的重绘,仅仅是Text才会重绘
然后我们就要说几个有疑问的地方了
1.使用MultiProvider和ChangeNotifierProvider来初始化有什么不同?
const MultiProvider({
Key key,
//list
@required this.providers,
this.child,
}) : assert(providers != null),
super(key: key);
ChangeNotifierProvider({
Key key,
@required ValueBuilder<T> builder,
Widget child,
}) : super(key: key, builder: builder, dispose: _disposer, child: child);
MultiProvider接收的参数是List,如果当前页面的数据来自多个Provider就使用它来初始化,比如我们在“我的”页面可能要显示用户信息(UserProvider)以及订单信息(OrderProvider)
ChangeNotifierProvider就简单了,指定单个Provider而已
2.ChangeNotifierProvider和ChangeNotifierProvider.value的区别
ChangeNotifierProvider({
Key key,
@required ValueBuilder<T> builder,
Widget child,
}) : super(key: key, builder: builder, dispose: _disposer, child: child);
/// Provides an existing [ChangeNotifier].
ChangeNotifierProvider.value({
Key key,
@required T value,
Widget child,
}) : super.value(key: key, value: value, child: child);
ChangeNotifierProvider(builder模式)的父类构造器多了一个disposer,当ChangeNotifierProvider从widget树中被移除时会自动调用dispose方法移除相应的数据,使得内存占用永远保持着一个合理的水平。
ChangeNotifierProvider.value在被移除widget树的时候不会自动调用dispose,需要手动去管理数据,这种方式适合一些老手,比如在被移除的时候依然有其它地方想使用这个数据,并在合适的时候再去手动关闭。
3.ChangeNotifierProvider中child参数下的widget不会因为数据的改变而重绘页面
上面的代码中我们把所有与UI相关的widget都放到了child下面,为什么只有Text发生了改变? 因为我们使用了Consumer来包裹,这样除开Consumer以外的内容都不会重绘,这也是为什么Provider能做到局部刷新UI。
所以我们在使用Consumer包裹内容的时候粒度要尽可能的细,要是直接包裹了全局那相当于没用Provider.
4.如何使用Consumer获取多个Provider
我们在计数器的例子中使用Consumer只获取了CounterProvider来给Text提供数据,但实际开发中我们的业务场景肯定会很复杂,使用Consumer包裹的widget需要多个Provider来提供数据咋办? 比如我们的Text需要显示count(来自CounterProvider),还有username(来自UserProvider)
Provider的作者当然也考虑到这一点,所以提供了多种Consumer来适应不同的场景

从Consumer——Consumer6,你最多可以一次性获取6个Provider,如果你要获取7个呢?抱歉,真没这种方式了,只能自力更生。。。。。
其实我也觉得这种实现方式不够优雅,但是6种已经能够满足99%的使用场景了!