美文网首页Flutter
Flutter状态管理之Riverpod

Flutter状态管理之Riverpod

作者: 唯鹿_weilu | 来源:发表于2020-09-03 09:25 被阅读0次
    River Deer

    最近一两个月在一些Flutter的话题中不断的见到了Riverpod这个关键词,细看后发现它是Flutter状态管理的一个新方式。

    Flutter的状态管理方式有很多,ReduxBlocMobXProvider等等。单单一个Provider,我也见到了各种组合,例如ChangeNotifier + Provider / StateNotifier + Provider( + freezed)。各种方式各有千秋,我们根据自己的习惯和项目的情况去选择就好,这里不做讨论。本篇只是来介绍一下Riverpod,给大家提供一个新的选择。

    1.介绍

    Riverpod logo

    RiverpodProvider师出同门,都来自作者RemiRiverpod可以被认为是Provider的重写,来实现原本不可能的功能。就像它的名字一样,字母与provider相同,但是又不相同。

    你可以理解RiverpodProvider的升级版,解决了Provider的一些痛点:

    • ProviderInheritedWidget的封装,所以在读取状态时需要BuildContext。这导致了许多的限制,许多新手在不理解InheritedWidgetBuildContext时,跨页面获取状态经常会ProviderNotFoundExceptionRiverpod不再依赖Flutter,也就是没有使用InheritedWidget,所以也不需要BuildContext

    • 读取对象是编译安全的。没有那么多的运行时异常。

    • 能够有多个相同类型的provider。

    • provider可以是私有的。

    • 当不再使用provider的状态时,将其自动回收。

    当然目前Riverpod也有一些不足(0.9.1版本):

    • 毕竟诞生不久,它还不能保证是完全稳定的。
    • 可能后期会有API的破坏性改动。(比如在0.7.0就有不少Breaking,导致我之前写的部分示例内容就报错了。)
    • 目前生产环境中使用需要谨慎。

    2.如何选择

    作者提供了Riverpod的三种方式,怎样选择如下图:

    如何选择Riverpod
    本篇不引入flutter_hooks相关内容,这里我就选择flutter_riverpod 。那么将它添加到pubspec.yaml中:
    flutter_riverpod: ^0.9.1
    

    然后执行flutter pub get

    3.基础使用

    Provider

    这里使用RiverpodProvider需要三步就可以。

    import 'package:flutter/material.dart';
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    
    // 1.创建一个全局的provider,里面储存“Hello World!”
    final Provider<String> helloWorldProvider = Provider((_) => 'Hello World!');
    
    void main() {
      runApp(
        // 2.添加“ProviderScope”。所有使用Riverpod的Flutter程序都必须
        // 在widget tree的根部添加它,用来储存各个provider。
        ProviderScope(
          child: MyApp(),
        ),
      );
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Riverpod Example',
          theme: ThemeData(
            primarySwatch: Colors.blue,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: ProviderExample(),
        );
      }
    }
    
    // 3.使用“ConsumerWidget”,在“build”中获取对应的provider
    class ProviderExample extends ConsumerWidget {
    
      @override
      Widget build(BuildContext context, ScopedReader watch) {
        final String value = watch(helloWorldProvider);
    
        return Scaffold(
          appBar: AppBar(title: Text('Provider Example')),
          body: Center(
            child: Text(value),
          ),
        );
      }
    }
    

    这里储存“Hello World!” 使用的是Provider,它提供一个永远不变的对象。不过大部分场景下状态都是可变的,下面用计数器来举例。

    StateProvider

    在“Hello World”的基础上,做两点修改即可。

    1. 定义一个全局常量StateProvider

      final StateProvider<int> counterProvider = StateProvider((_) => 0);
      
    2. import 'package:flutter/material.dart';
      import 'package:flutter_riverpod/flutter_riverpod.dart';
      
      class StateProviderExample extends StatelessWidget {
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(
              title: Text('StateProvider Example'),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Text(
                    'You have pushed the button this many times:',
                  ),
                  Consumer(
                    builder: (context, watch, _) {
                      /// 使用Consumer(ConsumerWidget的封装),控制刷新的范围。
                      int count = watch(counterProvider).state;
                      return Text(
                        '$count',
                        style: Theme.of(context).textTheme.headline4,
                      );
                    },
                  ),
                ],
              ),
            ),
            floatingActionButton: FloatingActionButton(
              /// 使用read获取counterProvider,操作state。
              onPressed: () => context.read(counterProvider).state++,
              tooltip: 'Increment',
              child: Icon(Icons.add),
            ),
          );
        }
      
      }
      

    如果你的状态比较复杂可以使用ChangeNotifierProvider,如果习惯使用StateNotifier,可以使用StateNotifierProvider 。其实StateProvider的内部是StateController,也还是StateNotifier。源码如下;

    class StateProvider<T>
        extends AlwaysAliveProviderBase<StateController<T>, StateController<T>> {
      
      StateProvider(
        Create<T, ProviderReference> create, {
        String name,
      }) : super((ref) => StateController(create(ref)), name);
     ...
    }
    
    class StateController<T> extends StateNotifier<T> {
      StateController(T state) : super(state);
    
      @override
      T get state => super.state;
    
      @override
      set state(T value) => super.state = value;
    }
    
    

    StateNotifierProvider的用法与StateProvider基本一致,这里就不贴出来了,有兴趣的可以点击这里查看

    ChangeNotifierProvider

    这部分没啥说的,注意ChangeNotifierStateNotifier的区别,需要自己调用notifyListeners通知变更。

    final ChangeNotifierProvider<Counter> _counterProvider = ChangeNotifierProvider((_) => Counter());
    
    class Counter extends ChangeNotifier {
      int _count = 0;
    
      int get count => _count;
    
      void increment() {
        _count++;
        notifyListeners();
      }
      void decrement(){
        _count--;
        notifyListeners();
      }
    }
    
    class ChangeProviderNotifierExample extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('ChangeNotifierProvider Example'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  'You have pushed the button this many times:',
                ),
                Consumer(
                  builder: (context, watch, _) {
                    int count = watch(_counterProvider).count;
                    return Text(
                      '$count',
                      style: Theme.of(context).textTheme.headline4,
                    );
                  },
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            /// 使用read获取counterProvider。
            onPressed: () => context.read(_counterProvider).increment(),
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
        );
      }
    }
    

    FutureProvider

    final FutureProvider<String> futureProvider = FutureProvider((_) async {
      /// 延时3s
      await Future.delayed(const Duration(seconds: 3));
      return 'Riverpod';
    });
    
    class FutureProviderExample extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('FutureProvider Example'),
          ),
          body: Center(
            child: Consumer(
              builder: (context, watch, _) {
                AsyncValue<String> futureProviderValue = watch(futureProvider);
                /// 根据相应状态展示
                return futureProviderValue.when(
                  loading: () => CircularProgressIndicator(),
                  error: (error, stack) => Text('Oops, something unexpected happened'),
                  data: (value) => Text(
                    'Hello $value',
                    style: Theme.of(context).textTheme.headline4,
                  ),
                );
              },
            ),
          ),
        );
      }
    }
    

    作者也提供了StreamProvider。用法大同小异,有兴趣的可以查看我的示例代码。

    ProviderListener

    如果你希望在Widget Tree上监听provider的状态变化,可以使用ProviderListener。用上面的计数器例子,当计数器为5时,触发监听。

    ProviderListener<StateController<int>>(
      provider: counterProvider,
      onChange: (_, counter) {
        if (counter.state == 5) {
          print('当前计数器为5,触发监听。');
        }
      },
      child: Consumer(
        builder: (context, watch, _) {
          int count = watch(counterProvider).state;
          return Text(
            '$count',
            style: Theme.of(context).textTheme.headline4,
          );
        },
      ),
    ),
    

    ScopeProvider

    一般我们在实现一个列表的Item时,需要传入相应的index大致如下:

    ListView.builder(
      itemCount: 50,
      itemBuilder: (context, index) {
        return ProductItem(index: index);
      },
    )
    

    如果使用ScopedProvider并结合 ProviderScope,就可以简单的获取index,不必从构造方法接收它。使用起来很简单,直接上代码:

    /// 定义ScopedProvider
    final ScopedProvider<int> currentProductIndex = ScopedProvider<int>(null);
    
    class ScopeProviderExample extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('ScopedProvider'),
          ),
          body: ListView.builder(
            itemCount: 50,
            itemBuilder: (context, index) {
              return ProviderScope(
                overrides: [
                  /// 修改value
                  currentProductIndex.overrideWithValue(index),
                ],
                /// 使用'const'关键字实例化了“ProductItem”,
                /// 但仍然可以在内部动态获取内容。
                child: const ProductItem(),
              );
            },
          ),
        );
      }
    }
    
    class ProductItem extends ConsumerWidget {
    
      const ProductItem({Key key}): super(key: key);
    
      @override
      Widget build(BuildContext context, ScopedReader watch) {
        /// 获取相应index
        final index = watch(currentProductIndex);
        return ListTile(title: Text('item $index'));
      }
    }
    

    4.修饰符

    family

    family的作用是可以在获取provider时可以添加一个参数。直接上例子,一看便知:

    /// 使用family,可以在获取provider时传入city
    final _weatherProvider = Provider.family<String, String>((ref, city) {
      return '$city (Sunny)';
    });
    
    class FamilyExample extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('Family')),
          body: Center(
            child: Consumer(
              builder: (context, watch, _) {
                /// 这里可以传参“London”
                final String weather = watch(_weatherProvider('London'));
                return Text('$weather',);
              },
            ),
          ),
        );
      }
    }
    

    注意: 使用family时传入的参数是有限制的。比如boolintdoubleString 、常量或是重写了==hashCode的不可变对象。

    autoDispose

    前面我们的例子中,创建的provider因为保存在Widget Tree的根部。所以即使页面关闭,再次进入页面时会获取之前的状态。

    这显然是不灵活的,那么这里就可以使用autoDispose,它可以在我们不再使用provider时,自动将其销毁。那么合理的使用它可以避免内存泄漏。

    比如之前的计数器例子,只需加一个autoDispose就可以避免此类问题。

    final stateProvider = StateProvider.autoDispose((_) => 0);
    

    如果你需要自定义dispose事件,可以使用onDispose。比如你的provider中有网络请求(使用Dio):

    final myProvider = FutureProvider.autoDispose((ref) async {
      
      final cancelToken = CancelToken();
      // 当provider被销毁时,取消http请求
      ref.onDispose(() => cancelToken.cancel());
    
      // http请求
      final response = await dio.get('path', cancelToken: cancelToken);
      // 如果请求成功完成,则保持该状态。
      ref.maintainState = true;
      return response;
    });
    

    上面代码中出现了ref.maintainState,这个参数默认为false。如果用户离开页面并且请求失败,下次则将再次执行该请求。但是,如果请求成功完成(maintainState为true),则将保留状态,下次重新进入页面时不会触发新的请求。

    使用autoDispose可以达到限制provider是全局还是局部作用。这样一来,可以更方便的解决跨页面使用provider的问题。

    5.进阶使用

    Combining providers

    1.如果创建的provider需要另一个provider的状态,这时就需要使用ProviderReferenceread方法。

    下面的示例是,给予城市和国家的provider,当创建locationProvider时,获取城市和国家的状态。

    final Provider<String> cityProvider = Provider((ref) => 'London');
    final Provider<String> countryProvider = Provider((ref) => 'England');
    final Provider<Location> locationProvider = Provider((ref) => Location(ref));
    
    class Location {
      Location(this._ref);
    
      final ProviderReference _ref;
    
      String get label {
        /// read 获取
        final city = _ref.read(cityProvider);
        final country = _ref.read(countryProvider);
        return '$city ($country)';
      }
    }
    

    使用Riverpod就可以提供多个相同类型的Provider,这也是相比Provider的一个优点。

    2.如果获取的状态值会发生变化,我们需要监听它。可以使用ProviderReferencewatch方法。

    下面的示例是,给予城市provider,当城市变化时,天气也相应变化。

    final StateProvider<String> cityProvider = StateProvider((ref) => 'London');
    final StateProvider<String> weatherProvider = StateProvider((ref) {
      /// watch监听
      final String city = ref.watch(cityProvider).state;
      return '$city (Sunny)';
    });
    
    class CombiningProviderExample2 extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('CombiningProvider')),
          body: Center(
            child: Consumer(
              builder: (context, watch, _) {
                final String weather = watch(weatherProvider).state;
                return Text('$weather',);
              },
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              String city = context.read(cityProvider).state;
                    /// 修改状态
              if (city == 'London') {
                context.read(cityProvider).state = "Xi'an";
              } else {
                context.read(cityProvider).state = 'London';
              }
            },
            tooltip: 'Refresh',
            child: Icon(Icons.refresh),
          ),
        );
      }
    }
    

    refresh

    强制provider立即刷新,重新返回创建的值。这种适合列表下拉刷新,或者请求数据错误时重试。

    
    final FutureProvider<List<String>> productsProvider = FutureProvider((_) async {
      /// 延时3s
      await Future.delayed(const Duration(seconds: 3));
      return List.generate(50, (index) => 'Item $index');
    });
    
    class RefreshProviderExample extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('RefreshProvider'),
          ),
          body: Center(
            child: Consumer(
              builder: (context, watch, _) {
                AsyncValue<List<String>> productsProviderValue = watch(productsProvider);
                return productsProviderValue.when(
                  loading: () => CircularProgressIndicator(),
                  error: (error, stack) => Text('Oops, something unexpected happened'),
                  data: (list) => RefreshIndicator(
                    onRefresh: () => context.refresh(productsProvider), /// 刷新
                    child: ListView(
                      children: [
                        for (final item in list) ListTile(title: Text(item)),
                      ],
                    ),
                  ),
                );
              },
            ),
          ),
        );
      }
    }
    

    select

    当状态中某一个值发生变化时,相应Consumer下的builder就会执行,重建widget。如果使用select可以指定某一值更改时进行刷新,精准控制刷新范围,避免不必要的rebuild。

    不过目前(0.9.1版本),select这种局部监听只支持使用hooks_riverpod包的useProvider。所以这里需要引用hooks_riverpod

    final ChangeNotifierProvider<Person> personProvider = ChangeNotifierProvider((_) => Person());
    
    class Person extends ChangeNotifier {
      int _age = 0;
      int get age => _age;
      set age(int age) {
        _age = age;
        notifyListeners();
      }
    
      String _name = 'weilu';
      String get name => _name;
      set name(String name) {
        _name = name;
        notifyListeners();
      }
    }
    
    class SelectExample extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Select Example'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                HookBuilder(
                  builder: (_) {
                    String name = useProvider(personProvider.select((p) => p.name));
                    /// 如果使用下面的方式,则age变化时,这里的Text也会刷新。
    //                String name = useProvider(personProvider).name;
                    return Text(
                      'name:$name',
                    );
                  },
                ),
                HookBuilder(
                  builder: (_) {
                    int age = useProvider(personProvider.select((p) => p.age));
                    return Text(
                      'age:$age',
                    );
                  },
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            // 这里age变化时,只有对应的Text会变化。
            onPressed: () => context.read(personProvider).age = Random.secure().nextInt(255),
            tooltip: 'Refresh',
            child: Icon(Icons.refresh),
          ),
        );
      }
    }
    
    

    其他

    细心的你会发现,在使用read获取provider时还是使用了context。一开始不是说,没有使用InheritedWidget,所以也不需要BuildContext吗?

    其实Riverpod本身确实如此,但是在Flutter的应用中,为了便于高效(时间复杂度O(1))的在Widget Tree中获取ProviderContainer(在ProviderScope中隐式创建,用来储存provider),需要在根部使用InheritedWidget,便于最终获取provider。

    readrefreshConsumerProviderListener等方法和Widget的内部其实都调用了ProviderScope.containerOf(context, listen = xx);,不同的是listen的值。

    static ProviderContainer containerOf(
        BuildContext context, {
        bool listen = true,
      }) {
        UncontrolledProviderScope scope;
        if (listen) {
          scope = context //
              .dependOnInheritedWidgetOfExactType<UncontrolledProviderScope>();
        } else {
          scope = context
              .getElementForInheritedWidgetOfExactType<UncontrolledProviderScope>()
              .widget as UncontrolledProviderScope;
        }
        return scope.container;
      }
    

    比如read中listen的值为false,使用getElementForInheritedWidgetOfExactType方法,这样在数据发生变化时就不会掉用didChangeDependencies,避免不必要的rebuild。相对的,ConsumerProviderListener中listen的值为ture,会实现我们需要的widget重建。

    我们可以还可以通过Flutter Inspector检查已有的状态,所有状态汇总在ProviderScope下面,这也是Riverpod的一个优点。如下图所示:

    Flutter Inspector

    发布本篇时,有关Riverpod的资料与讨论很少。本篇也是我在实践完官网文档后的理解,如有错误,欢迎指出!

    个人认为Riverpod是相对更轻松便捷的一种状态管理方式,待它稳定时应该能被更多的人喜爱。

    Riverpod的相关示例代码我已经上传至Github,有兴趣的可以看看。后面如果Riverpod有变动时,我也会及时更新。大家可以收藏起来,多多点赞支持一下,给我点更新动力!

    参考

    相关文章

      网友评论

        本文标题:Flutter状态管理之Riverpod

        本文链接:https://www.haomeiwen.com/subject/dtlnsktx.html