之前的文章写了InheritedWidget in practice,讲述了怎么跨Widget共享和更新数据,有兴趣的朋友可以看看。InheritedWidget使用起来太绕,而且天生不适合改变的数据,本文就祭上官方推荐的大杀器Provider, Provider以更优雅的方式实现跨Widget的数据共享和更新。
参考
关于Provider的原理大家可以自行Google,本文根据使用场景献上几种Provider的操作方式,Demo地址:gitee.com
跨Widget读取静态数据
场景:有个共享数据的对象NormalModel, 它有四个属性val1, val2, val3, val4, NormalWidget负责显示NormalModel里的数据, 为了避免超大Widget的存在,将四个属性分别新建子Widget来显示,Normal显示val1, Widget1显示val2..。
模拟现实场景如个人资料页,需要显示头像、昵称、部门等信息,每个信息都由不同的Widget负责显示。
- 新建待共享的对象
class NormalModel {
String val1;
String val2;
String val3;
String val4;
NormalModel(this.val1, this.val2, this.val3, this.val4);
}
- 使用
context.select()使用共享的属性(代码已简化,完整代码请查看demo)
class Normal extends StatelessWidget {
@override
Widget build(BuildContext context) {
var value = context.select<NormalModel, String>((p) => p.val1);
...
Column(children: [Text(value), Widget1()]),
...
}
}
class Widget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
var value = context.select<NormalModel, String>((p) => p.val2);
...
Column(children: [Text(value), Widget2()]),
...
}
}
class Widget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
var value = context.select<NormalModel, String>((p) => p.val3);
...
Column(children: [Text(value), Widget3()]),
...
}
}
class Widget3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
var value = context.select<NormalModel, String>((p) => p.val4);
...
Text(value),
...
}
}
- 路由显示
NormalWidget,将Normal设置为Provider的child, 使用Provider.value()创建静态的Provider。
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Provider.value(
value: NormalModel('Hello', 'China', '你好', '中国'),
child: Normal())));
可变数据
场景:Flutter新建项目得到的counter示例,单击FloatActionButton count会自增并显示。
模拟现实场景如购物车,将数据加入列表中。
- 新建类
CountProvider继承ChangeNotifier, 提供方法plus(), 每次_count变更后调用notifyListeners()告知数据已更新。
class CountProvider extends ChangeNotifier {
int _count = 0;
int get count => _count;
void plus() {
_count++;
notifyListeners();
}
}
- 新建
ChangeNotifyWidgetWidget用来展示count和自增count。
- 使用
context.read<CountProvider>().plus()增加count(read不会监听,用完即弃)。 - 使用
context.select<CountProvider, int>((p) => p.count)读取count。使用select将会监听count的变化,在count更新时调用build()方法刷新界面。
class ChangeNotifyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ChangeNotify')),
body: Center(
child: _CountLabel(),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CountProvider>().plus(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class _CountLabel extends StatelessWidget {
@override
Widget build(BuildContext context) {
var count = context.select<CountProvider, int>((p) => p.count);
return Text('$count');
}
}
- 路由显示Widget。
使用ChangeNotifierProvider创建监听变化的Provider,create()返回provider, 并将要使用该provider的widget设置其child。
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (_) => CountProvider(),
child: ChangeNotifyWidget())));
数组
场景:ListView展示一个动态列表,单击➕在末尾增加一个元素,单击列表行会触发行数据更新,长按列表行删除该行。
模拟现实场景如购物车,包括诸如加入购物车、移除购物车、更新购物车内商品数量等常用操作。
- 新建用于展示的数据
Person,核心属性只有name,用于展示在列表中。
class Person {
String name;
Person(this.name);
@override
int get hashCode => name.hashCode;
@override
bool operator ==(Object other) {
if (other.runtimeType == runtimeType && other is Person) {
return other.name == name;
}
return false;
}
}
- 新建ChangeNotifier用于管理列表数据。
- 提供列表默认值。
- 支持新增、删除、更新、获取等方法。
class PersonProvider extends ChangeNotifier {
PersonProvider() {
for (var i = 0; i < 3; i++) {
_items.add(Person('$i'));
}
}
List<Person> _items = [Person('xxx'), Person('yyyy')];
List<Person> get items => _items;
void add(List<Person> items) {
if (items is List && items.isNotEmpty) {
_items.addAll(items);
notifyListeners();
}
}
void remove(int index) {
if (items is List && items.isNotEmpty) {
_items.removeAt(index);
notifyListeners();
}
}
void update(Person person, int index) {
_items[index] = person;
notifyListeners();
}
void updatePerson(Person person, String name) {
_items.where((element) => element == person).map((e) => e.name = name);
notifyListeners();
}
void updateName(String name, int index) {
_items[index].name = name;
notifyListeners();
}
Person getItem(int index) {
if (index < 0 || index >= _items.length) return null;
return _items[index];
}
}
- 创建StatefullWidget
ListProviderWidget展示列表数据。
-
列表页Widget关注
count, 这样当数量变化时会触发列表页的刷新(新增或移除)。var count = context.select<PersonProvider, int>((value) => value.items.length); -
列表项
_Cell, 通过index动态获取对应行的数据Model的对应属性, 当属性值发生变更时就会触发_Cell的build()方法:String name = context .select<PersonProvider, String>((value) => value.getItem(index)?.name); -
新增数据:
context.read<PersonProvider>().add(..); -
更新数据:
context .read<PersonProvider>() .updateName('${Random().nextInt(10000)}', index); -
删除数据:
context.read<PersonProvider>().remove(index)
class ListProviderWidget extends StatefulWidget {
@override
_ListProviderWidgetState createState() => _ListProviderWidgetState();
}
class _ListProviderWidgetState extends State<ListProviderWidget> {
var random = Random();
void _incrementCounter() {
var max = 1000000;
context.read<PersonProvider>().add([Person('${random.nextInt(max)}')]);
}
@override
Widget build(BuildContext context) {
print('build_ home page');
var count =
context.select<PersonProvider, int>((value) => value.items.length);
return Scaffold(
appBar: AppBar(
title: Text('List ChangeNotifierProvider'),
),
body: ListView.builder(
itemCount: count,
itemBuilder: (BuildContext context, int index) {
return _Cell(index);
}),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class _Cell extends StatelessWidget {
final int index;
_Cell(this.index);
@override
Widget build(BuildContext context) {
print('build_ cell $index');
String name = context
.select<PersonProvider, String>((value) => value.getItem(index)?.name);
if (name == null) return Container();
return ListTile(
title: Text(name),
onTap: () {
context
.read<PersonProvider>()
.updateName('${Random().nextInt(10000)}', index);
},
onLongPress: () => context.read<PersonProvider>().remove(index),
);
}
}
代理和静态数据
场景:根据不同的颜色会生成新的列表。
模拟现实场景:邮件列表页会因为当前选择文件夹的变更而更新数据。
- 创建颜色代理类
ProxyColor, 当颜色变更时会更新_items数据
class ProxyColor {
List<String> _items = [];
List<String> get items => _items;
void update(String color) {
List<String> result = [];
for (var i = 0; i < 20; i++) {
result.add('$color $i');
}
_items = result;
}
String getItemAt(int index) {
return _items[index];
}
}
- 展示颜色列表
class ProxyProviderWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
var count = context.select<ProxyColor, int>((p) => p.items.length);
return Scaffold(
appBar: AppBar(title: Text('ProxyProvider')),
body: ListView.builder(
itemCount: count, itemBuilder: (context, index) => _Cell(index)),
);
}
}
class _Cell extends StatelessWidget {
final int index;
_Cell(this.index);
@override
Widget build(BuildContext context) {
var value = context.select<ProxyColor, String>((p) => p.getItemAt(index));
return ListTile(title: Text(value));
}
}
- 使用
ProxyProvider基于已存在provider生成一个新的provider
var colors = ['red', 'green', 'blue'];
var index = Random().nextInt(3);
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => MultiProvider(providers: [
Provider.value(value: colors[index]),
ProxyProvider<String, ProxyColor>(
update: (context, color, proxyProvider) {
var provider = ProxyColor();
provider.update(color);
return provider;
})
], child: ProxyProviderWidget())));
代理和可变数据
场景:根据不同的颜色得到新的Provider, 支持动态更新一些其他数据。
- 新建Provider,
_color或者_count变更时触发notifyListeners()通知外部刷新界面。
class ColorCountProvider extends ChangeNotifier {
int _count = 0;
String _color = 'black';
String get color => _color;
set color(String color) {
_color = color;
notifyListeners();
}
String get value => '$_color $_count';
void plus() {
_count++;
notifyListeners();
}
}
- 新建Widget用于读取count和颜色值,并提供方法增加count值
class ChangeNotifyProxyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ChangeNotifyProxyProvider')),
body: Center(
child: _CountLabel(),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<ColorCountProvider>().plus(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class _CountLabel extends StatelessWidget {
@override
Widget build(BuildContext context) {
var count = context.select<ColorCountProvider, String>((p) => p.value);
return Text('$count');
}
}
- 路由打开Widget, 使用
ChangeNotifierProxyProvider的create返回provider, 使用update基于前一个provider来刷新当前provider的值。
var colors = ['red', 'green', 'blue'];
var index = Random().nextInt(3);
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => MultiProvider(providers: [
Provider.value(value: colors[index]),
ChangeNotifierProxyProvider<String,
ColorCountProvider>(
create: (context) => ColorCountProvider(),
update: (context, value, provider) {
provider.color = value;
return provider;
})
], child: ChangeNotifyProxyWidget())));
总结
-
provider的三个方法(其实平常只用2个)。 方法 | 说明 --- | --- read() | 一次性的获取provider, 一般用于调用provider的方法。
select() | 获取并监听需要的属性,在属性更新时触发当前widget的build方法。
watch() |不建议使用,假设多个属性都会触发notifyListener,则当前无关属性更新时也会触发build方法。 -
使用Provider管理列表数据。
- ListView对应的widget使用select得到count, 这样当count变更时(删除、新增)就会触发ListView对应Widget的build方法,刷新整个列表的数据。
- 列表项如果有child的也一样,通过index关注对应对应相关的属性即可,不要直接将model都传入到列表项。
- 几种Provider的选择
- 实际最常用的是这个
ChangeNotifierProxyProvider和MultiProvider, 比如搜索,通过Provider.value()传入搜索条件,使用ChangeNotifierProxyProvider根据搜索条件进行搜索并通知外部更新UI。 - Provider.value用于共享已经存在的对象。
- ProxyProvider配合Provider.value生成新的Provider.
- ChangeNotifierProxyProvider只是ProxyProvider的变种,新的Provider支持notifyListener()
- Provider限定了各种使用,在调试时使用不当会有错误信息,请注意信息并调整使用方式。