riverpod1.0+简介

Flutter更新到2.8了,最近打算重拾一下flutter,写点东西练练手。大家都清楚在flutter中状态管理确实挺麻烦的,从一开始的BLoC到provide、Provider,还有getX、Riverpod等等各式各样的状态管理库,我个人倾向于使用riverpod,它更像一个状态管理库;而getX更像一个开发的框架,实在是太大了:当你使用getX的时候,你是在用getX而不是flutter写应用。

引入riverpod

demo中没有包含flutter_hook,所以我们选择引入flutter_riverpod即可

1
2
3
4
5
6
7
8
environment:
sdk: ">=2.15.1 <3.0.0"
flutter: ">=2.0.0"

dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^1.0.3

hello world

首先,我们需要使用ProviderScope来包裹整个应用,也就是在main方法中

1
2
3
void main() {
runApp(ProviderScope(child: Home()));
}

然后我们可以声明一个Provider。一般情况下,我们会把各种各样的provider作为全局变量来引用,声明一个provider和声明一个函数没有多大的区别。

1
final helloWorldProvider = Provider((_) => 'Hello world');

最后我们就可以读取Provider中的数据了。
在1.0.0之后的版本中,ConsumerWidget的build方法中提供了WidgetRef对象,用来取代0.14版本中的useProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final helloWorldProvider = Provider((_)=>"hello world");

void main() {
runApp(const ProviderScope(child: Home()));
}

class Home extends ConsumerWidget{
const Home({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {

final String value = ref.watch(helloWorldProvider);

return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("riverpod demo"),),
body: Center(
child: Text(value),
),
),
);
}
}

Provider

各种各样的Provider

具体可以看这里,https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/flutter_riverpod-library.html

下面列举了一些常用的Provider类型

https://pub.dev/documentation/riverpod/latest/riverpod/StateProvider-class.html

Provider的修饰符

.family

使用

该修饰符适用于适用外部数据来构建provider的情况

一些常用情况

  • FutureProvider 组合,来根据id获取消息
  • 把当前Locale对象传给provider,用来进行国际化
  • 在不访问对方属性的前提下连接两个provider

在使用family时,会额外的向provider提供一个属性,在provider中我们可以自由的使用该属性来创建某些状态

1
2
3
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
return dio.get('http://my_api.dev/messages/$id');
});

这种情况下在使用messagesFamily时会有点语法上的变化,我们需要额外提供一个参数

1
2
3
Widget build(BuildContext context, WidgetRef ref) {
final response = ref.watch(messagesFamily('id'));
}

它还支持同时获取不同的属性

1
2
3
4
5
6
7
@override
Widget build(BuildContext context, WidgetRef ref) {
final frenchTitle = ref.watch(titleFamily(const Locale('fr')));
final englishTitle = ref.watch(titleFamily(const Locale('en')));

return Text('fr: $frenchTitle en: $englishTitle');
}
参数限制

参数不限制类型,但必须实现==hashCode两个方法;

如果参数不是constant的,比如我们想将输入框内容传给Provider,但是输入框的内容会变化的特别频繁并且不能复用,这种情况可能会导致内存泄露,可以使用.autoDispose修饰符来修复这个问题

1
2
3
final characters = FutureProvider.autoDispose.family<List<Character>, String>((ref, filter) async {
return fetchCharacters(filter: filter);
});
传递多个参数

.family修饰符并没有内置提供过个参数的方法,另外一方面,这个参数可以是任意符合上面提到的限制的类型。
比如

  • 元组
  • 使用 Freezed 或 built_value 生成的对象
  • 使用 equatable 的对象

** freezed **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@freezed
abstract class MyParameter with _$MyParameter {
factory MyParameter({
required int userId,
required Locale locale,
}) = _MyParameter;
}

final exampleProvider = Provider.autoDispose.family<Something, MyParameter>((ref, myParameter) {
print(myParameter.userId);
print(myParameter.locale);
// Do something with userId/locale
});

@override
Widget build(BuildContext context, WidgetRef ref) {
int userId; // Read the user ID from somewhere
final locale = Localizations.localeOf(context);

final something = ref.watch(
exampleProvider(MyParameter(userId: userId, locale: locale)),
);

...
}

Equatable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class MyParameter extends Equatable  {
MyParameter({
required this.userId,
required this.locale,
});

final int userId;
final Locale locale;

@override
List<Object> get props => [userId, locale];
}

final exampleProvider = Provider.family<Something, MyParameter>((ref, myParameter) {
print(myParameter.userId);
print(myParameter.locale);
// Do something with userId/locale
});

@override
Widget build(BuildContext context, WidgetRef ref) {
int userId; // Read the user ID from somewhere
final locale = Localizations.localeOf(context);

final something = ref.watch(
exampleProvider(MyParameter(userId: userId, locale: locale)),
);

...
}

.autoDispose

一个通用场景是能够自动释放长时间不适用Provider;

有很多个让我们这么做得理由,比如:

  • 在使用Firebase时,关闭连接避免不必要的开销
  • 当用户离开页面再进入页面时重置状态

我们可以使用内嵌的.autoDispose修饰符来支持上述场景

使用

想要告诉Riverpod在不再使用provider时将其销毁,只需要在Provider之前加上.autoDispose即可

1
2
3
final userProvider = StreamProvider.autoDispose<User>((ref) {

});

就这样,当userProvider不再使用时将会被自动销毁

注意通用参数是如何在autoDispose之后而不是之前传递的–autoDispose不是一个命名的构造函数。

当然,上面也提到可以和其他修饰符一起

1
2
3
final userProvider = StreamProvider.autoDispose.family<User, String>((ref, id) {

});
ref.maintainState

autoDispose标记一个提供者,也会在ref上增加一个额外的属性: maintainState

该属性是一个布尔值(默认为false),允许提供者告诉Riverpod即使不再被监听,是否应该保留提供者的状态。

一个用例是在一个HTTP请求完成后,将这个标志设置为true:

1
2
3
4
5
final myProvider = FutureProvider.autoDispose((ref) async {
final response = await dio.get(...);
ref.maintainState = true;
return response;
});

这样,如果请求失败,用户离开屏幕后又重新进入,那么请求将被再次执行。但如果请求成功完成,状态将被保留,重新进入屏幕将不会触发新的请求。

示例:取消http请求

autoDispose修改器可以与FutureProvider和ref.onDispose相结合,以便在不再需要HTTP请求时轻松取消。

要求:

  • 当用户进入一个屏幕时,启动一个HTTP请求
  • 如果用户在请求完成前离开屏幕,则取消HTTP请求
  • 如果请求成功,离开并重新进入屏幕不会启动一个新的请求
1
2
3
4
5
6
7
8
9
10
11
12
final myProvider = FutureProvider.autoDispose((ref) async {
// An object from package:dio that allows cancelling http requests
final cancelToken = CancelToken();
// When the provider is destroyed, cancel the http request
ref.onDispose(() => cancelToken.cancel());

// Fetch our data and pass our `cancelToken` for cancellation to work
final response = await dio.get('path', cancelToken: cancelToken);
// If the request completed successfully, keep the state
ref.maintainState = true;
return response;
});
参数类型’AutoDisposeProvider’不能分配给参数类型’AlwaysAliveProviderBase’。

当使用.autoDispose时,你可能会发现自己的应用程序无法编译,出现类似的错误。

The argument type ‘AutoDisposeProvider’ can’t be assigned to the parameter type ‘AlwaysAliveProviderBase’

可能是因为你试图在一个没有标记为.autoDispose的提供者中监听一个标记为.autoDispose的提供者,例如:

1
2
3
4
5
6
7
final firstProvider = Provider.autoDispose((ref) => 0);

final secondProvider = Provider((ref) {
// The argument type 'AutoDisposeProvider<int>' can't be assigned to the
// parameter type 'AlwaysAliveProviderBase<Object, Null>'
ref.watch(firstProvider);
});

这是不可取的,因为它将导致firstProvider永远不会被dispose。我们可以考虑将 secondProvider 标记为 `.autoDispose来修复这个问题:

1
2
3
4
5
final firstProvider = Provider.autoDispose((ref) => 0);

final secondProvider = Provider.autoDispose((ref) {
ref.watch(firstProvider);
});

WidgetRef

获取WidgetRef对象

从其他Provider对象中获取

1
2
3
4
5
final provider = Provider((ref) {
// use ref to obtain other providers
final repository = ref.watch(repositoryProvider);
return SomeValue(repository);
})

ref对象可以很安全的在provider之间传递,一个常见的用法就是讲ref传递给 StateNotifier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final counter = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});

class Counter extends StateNotifier<int> {
Counter(this.ref): super(0);

final Ref ref;

void increment() {
// Counter can use the "ref" to read other providers
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}

这么做可以让Counter内部读取provider状态

从Widget对象中获取ref

一般情况下Widget对象中是没有ref对象中,但riverpod提供了几种解决方案

  • 使用ConsumerWidget替换StatelessWidget

ConsumerWidget和StatelessWidget基本相同(虽然是继承了StatefulWidget),只是在build方法中多了一个WidgetRef对象

1
2
3
4
5
6
7
8
9
10
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
// use ref to listen to a provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
  • 使用ConsumerStatefulWidget+ConsumerState 替换 StatefulWidget+State
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}): super(key: key);

@override
HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
// "ref" can be used in all life-cycles of a StatefulWidget.
ref.read(counterProvider);
}

@override
Widget build(BuildContext context) {
// We can also use "ref" to listen to a provider inside the build method
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

  • 使用 HookConsumerWidget 替换 HookWidget
1
2
3
4
5
6
7
8
9
10
11
12
13
class HomeView extends HookConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
// HookConsumerWidget allows using hooks inside the build method
final state = useState(0);

// We can also use the ref parameter to listen to providers.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

WidgetRef对象的方法

这里的WidgetRef对象在读取Provider中的数据时,提供了readlistenwatch方法。至于什么情况下选用哪个方法,这里有三个建议

  • 当我们需要监听变化并且从Provider中获取数据时,比如当数据变化时我们需要重新构建Widget,这时我们可以使用ref.watch
  • 当我们需要监听变化去执行某个动作时,我们可以使用ref.listen
  • 当我们仅需要读取数据不关心数据的变化时,比如点击某个按钮时,根据状态来判断下一步动作时,我们可以使用ref.read
  • ref.watch
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    final counterProvider = StateProvider((_)=> 0);
    class Home extends ConsumerWidget{
    const Home({Key? key}) : super(key: key);
    @override
    Widget build(BuildContext context, WidgetRef ref) {
    final int count = ref.watch(counterProvider);
    return MaterialApp(
    home: Scaffold(
    appBar: AppBar(title: const Text("riverpod demo"),),
    body: Center(
    child: Column(
    children: [
    Text('$count')
    ],
    ),
    ),
    floatingActionButton: FloatingActionButton(onPressed: ()=>{
    ref.read(counterProvider.state).state++
    },child: const Text("点击"),),
    ),
    );
    }
    }
  • ref.read

使用该方法可以没有任何影响的获取一次provider的状态,但是作者提示我们尽量不要使用该方法,它只是用来解决使用watch|listen不方便的问题,如果可以,尽量使用watch|listen.这里有个使用read方法的示例https://riverpod.dev/docs/concepts/combining_providers#can-i-read-a-provider-without-listening-to-it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());

class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// Call `increment()` on the `Counter` class
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
  • ref.listen

ref.watch相似,我们也可以使用ref.listen来观察provider。他们的区别就是当provider状态变化时,我们可以调用自己定义的方法。该方法需要两个参数,第一个参数是要监听的provider对象,第二个参数是回调方法,

1
2
3
4
5
6
7
8
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());

final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed ${newCount}');
});
...
});

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter());

class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed ${newCount}');
});
...
}
}

决定订阅什么

比如我们有一个StreamProvider

1
final userProvider = StreamProvider<User>(...);

我们可以这么去订阅

  • 通过监听provider本身来同步获取当前状态
1
2
3
4
5
6
7
8
9
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<User> user = ref.watch(userProvider);

return user.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => const Text('Oops'),
data: (user) => Text(user.name),
);
}
  • 通过监听userProvider.stream来获取对应的stream
1
2
3
Widget build(BuildContext context, WidgetRef ref) {
Stream<User> user = ref.watch(userProvider.stream);
}
  • 通过监听userProvider.future来获取一个能得到最新状态的Future
1
2
3
Widget build(BuildContext context, WidgetRef ref) {
Future<User> user = ref.watch(userProvider.future);
}

使用”select” 来决定哪些值变化时进行重建

比如我们有一个User对象

1
2
3
4
abstract class User {
String get name;
int get age;
}

但是我们在渲染页面时只用到了name属性

1
2
3
4
Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}

这种情况下,如果age属性发生了变化,该Widget就会重建,显然这不是我们想要的。这时候我们可以使用select来选择对象的某些属性来监听

1
2
3
4
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name))
return Text(name);
}

当然,select同样适用于listen方法

1
2
3
4
5
6
ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);

需要注意的是,这里没必要一定返回对象的属性,只要复写了==的值都可以正常工作,比如

final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));

riverpod1.0+简介
https://blog.huangyuanlove.com/2022/01/04/riverpod1-0-简介/
作者
HuangYuan_xuan
发布于
2022年1月4日
许可协议
BY HUANG兄