Flutter状态管理之Riverpod

前言

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

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

介绍


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

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

  • Provider是InheritedWidget的封装,所以在读取状态时需要BuildContext。这导致了许多的限制,许多新手在不理解InheritedWidget和BuildContext时,跨页面获取状态经常会ProviderNotFoundException。而Riverpod不再依赖Flutter,也就是没有使用InheritedWidget,所以也不需要BuildContext。
  • 读取对象是编译安全的。没有那么多的运行时异常。
  • 能够有多个相同类型的provider。
  • provider可以是私有的。
  • 当不再使用provider的状态时,将其自动回收

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

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

2.如何选择

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

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

flutter_riverpod: ^0.9.1

然后执行flutter pub get。

3.基础使用

Provider
这里使用Riverpod的Provider需要三步就可以。

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”的基础上,做两点修改即可。

定义一个全局常量StateProvider。

final StateProvider<int> counterProvider = StateProvider((_) => 0);
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
这部分没啥说的,注意ChangeNotifier与StateNotifier的区别,需要自己调用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),
      ),
    );
  }
}

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',);
          },
        ),
      ),
    );
  }
}

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的状态,这时就需要使用ProviderReference的read方法。

下面的示例是,给予城市和国家的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.如果获取的状态值会发生变化,我们需要监听它。可以使用ProviderReference的watch方法。

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

 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。

read、refresh、Consumer、ProviderListener等方法和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。相对的,Consumer、ProviderListener中listen的值为ture,会实现我们需要的widget重建。

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

以上就是本文的全部内容,希望对大家有所帮助

原文地址:https://segmentfault.com/a/1190000023872898

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值