Flutter Provider in practice

1,680 阅读5分钟

之前的文章写了InheritedWidget in practice,讲述了怎么跨Widget共享和更新数据,有兴趣的朋友可以看看。InheritedWidget使用起来太绕,而且天生不适合改变的数据,本文就祭上官方推荐的大杀器ProviderProvider以更优雅的方式实现跨Widget的数据共享和更新。

参考

关于Provider的原理大家可以自行Google,本文根据使用场景献上几种Provider的操作方式,Demo地址:gitee.com

  1. 跨Widget读取静态数据
  2. 可变数据
  3. 数组
  4. 代理和静态数据
  5. 代理和可变数据

跨Widget读取静态数据

场景:有个共享数据的对象NormalModel, 它有四个属性val1, val2, val3, val4, NormalWidget负责显示NormalModel里的数据, 为了避免超大Widget的存在,将四个属性分别新建子Widget来显示,Normal显示val1, Widget1显示val2..。
模拟现实场景如个人资料页,需要显示头像、昵称、部门等信息,每个信息都由不同的Widget负责显示。

  1. 新建待共享的对象
class NormalModel {
  String val1;
  String val2;
  String val3;
  String val4;

  NormalModel(this.val1, this.val2, this.val3, this.val4);
}
  1. 使用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),
	...
  }
}
  1. 路由显示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会自增并显示。
模拟现实场景如购物车,将数据加入列表中。

  1. 新建类CountProvider继承ChangeNotifier, 提供方法plus(), 每次_count变更后调用notifyListeners()告知数据已更新。
class CountProvider extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void plus() {
    _count++;

    notifyListeners();
  }
}
  1. 新建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');
  }
}
  1. 路由显示Widget。
    使用ChangeNotifierProvider创建监听变化的Providercreate()返回provider, 并将要使用该provider的widget设置其child
Navigator.of(context).push(MaterialPageRoute(
                      builder: (context) => ChangeNotifierProvider(
                          create: (_) => CountProvider(),
                          child: ChangeNotifyWidget())));

数组

场景:ListView展示一个动态列表,单击➕在末尾增加一个元素,单击列表行会触发行数据更新,长按列表行删除该行。
模拟现实场景如购物车,包括诸如加入购物车、移除购物车、更新购物车内商品数量等常用操作。

  1. 新建用于展示的数据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;
  }
}
  1. 新建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];
  }
}
  1. 创建StatefullWidgetListProviderWidget展示列表数据。
  • 列表页Widget关注count, 这样当数量变化时会触发列表页的刷新(新增或移除)。var count = context.select<PersonProvider, int>((value) => value.items.length);

  • 列表项_Cell, 通过index动态获取对应行的数据Model的对应属性, 当属性值发生变更时就会触发_Cellbuild()方法: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),
    );
  }
}

代理和静态数据

场景:根据不同的颜色会生成新的列表。
模拟现实场景:邮件列表页会因为当前选择文件夹的变更而更新数据。

  1. 创建颜色代理类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];
  }
}
  1. 展示颜色列表
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));
  }
}
  1. 使用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, 支持动态更新一些其他数据。

  1. 新建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();
  }
}
  1. 新建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');
  }
}
  1. 路由打开Widget, 使用ChangeNotifierProxyProvidercreate返回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())));

总结

  1. provider的三个方法(其实平常只用2个)。 方法 | 说明 --- | --- read() | 一次性的获取provider, 一般用于调用provider的方法。
    select() | 获取并监听需要的属性,在属性更新时触发当前widget的build方法。
    watch() |不建议使用,假设多个属性都会触发notifyListener,则当前无关属性更新时也会触发build方法。

  2. 使用Provider管理列表数据。

  • ListView对应的widget使用select得到count, 这样当count变更时(删除、新增)就会触发ListView对应Widget的build方法,刷新整个列表的数据。
  • 列表项如果有child的也一样,通过index关注对应对应相关的属性即可,不要直接将model都传入到列表项。
  1. 几种Provider的选择
  • 实际最常用的是这个ChangeNotifierProxyProviderMultiProvider, 比如搜索,通过Provider.value()传入搜索条件,使用ChangeNotifierProxyProvider根据搜索条件进行搜索并通知外部更新UI。
  • Provider.value用于共享已经存在的对象。
  • ProxyProvider配合Provider.value生成新的Provider.
  • ChangeNotifierProxyProvider只是ProxyProvider的变种,新的Provider支持notifyListener()
  1. Provider限定了各种使用,在调试时使用不当会有错误信息,请注意信息并调整使用方式。