Riverpod之Provider(一),使用Provider讲解了WidgetRef和Ref的watch要点
Riverpod之StateProvider(二),讲解了StateProvider的内部流程,主要涉及的是其内部的名叫state的Provider。
Riverpod之Provider&StateProvider(三),讲解了Provider和StateProvider的组合使用。
Riverpod之StateNotifierProvider(四),介绍了StateNotifierProvider的使用。
Riverpod之FutureProvider(五),介绍了FutureProvider的使用。
Riverpod之select(六),介绍了Provider的select方法使用和原理。
Riverpod之family(七),介绍了family方法得使用和内部流程
Riverpod之autoDispose(八),介绍了autoDispose方法的使用和内部流程
Riverpod之override(九),介绍了override属性的使用和内部流程
前言
override算是Riverpod里面的一个高级技能,你要是不在代码里弄上一点,你都不好意思跟别人说你熟悉Riverpod,作为最后一篇,我们还是从Demo开始,由浅入深的了解一下override的妙用。
Demo
override的使用一般就三步:
- 第一步:根据需求选择一个Provider,如果没有初始化值就先抛出异常
final itemProvider = Provider((ref) { throw UnimplementedError(); })
- 第二步:ProviderScope中有个overrides集合,专门用来复写Provider。Scope是范围的意思,所以这个复写的有效性也就局限在此ProviderScope中,超过这个范围使用Provider是无效的
ProviderScope(
overrides: [
itemProvider.overrideWithValue(numbers[index]),
],
child: item,
)
- 第三步:在ProviderScope子组件中使用被复写的Provider
String title = ref.watch(itemProvider);
Demo的效果如下,就是使用ListView展示一个列表,列表Item的数据来源于Provider而不是传参
代码入口
void main() {
runApp(const ProviderScope(child: OverrideApp()));
}
class OverrideApp extends StatelessWidget {
const OverrideApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(home: _Home());
}
}
主要代码
// 30条模拟数据
List<String> numbers = List.generate(30, (index) => index.toString());
// 空壳,目前还不知道提供什么数据,用于运行时被覆盖
final itemProvider = Provider<String>((ref) {
throw UnimplementedError();
});
class _Home extends ConsumerWidget {
const _Home();
@override
Widget build(BuildContext context, WidgetRef widgetRef) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: ListView.separated(
itemCount: numbers.length,
itemBuilder: (context, index) {
//关键点 item使用const关键字修饰
var item= const Item();
print("${identityHashCode(item)} --- ${numbers[index]}");
// 核心代码 在ProviderScope中对itemProvider进行复写
return ProviderScope(
overrides: [
itemProvider.overrideWithValue(numbers[index]),
],
child: item,
);
},
separatorBuilder: (context, _) {
return const Divider(
height: 30,
color: Color(0xff3d3d3d),
);
},
),
);
}
}
class Item extends ConsumerWidget {
const Item({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// itemProvider没有初始化,在使用之前一定要被复写,才能直接从itemProvider中获取数值,否则会报错
String title = ref.watch(itemProvider);
return Text(title,textAlign: TextAlign.center,);
}
}
使用上面这种方式有什么好处呢,还不如下面直接给Item传参来的简便
...
itemBuilder: (context, index) {
var item= Item(title:numbers[index]);
print("${identityHashCode(item)} --- ${numbers[index]}");
return item;
}
...
class Item extends ConsumerWidget {
final String title;
const Item({required this.title,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Text(title,textAlign: TextAlign.center,);
}
}
其实区别就在Item不能使用const修饰了,它不再是个常量,这个影响有多大,对比看看下面这个日志打印就知道const修饰的item自始至终都是一个对象,而非const修饰的不停的在新建对象
print("${identityHashCode(item)} --- ${numbers[index]}");
| const | 非const |
|---|---|
| 595695877 --- 0 | 878140213 --- 0 |
| 595695877 --- 1 | 1039269752 --- 1 |
| 595695877 --- 2 | 75365563 --- 2 |
| 595695877 --- 3 | 995618539 --- 3 |
| 595695877 --- 4 | 766272018 --- 4 |
| 595695877 --- 5 | 979290826 --- 5 |
所以代码绕了一圈就是为了可以使用const修饰Item,这样可以提高性能,当然override可不是专门用来干这事的。
有时候端侧和后台约定了接口但还没实现,这种情况可以在本地使用数据模拟调试,这也是override大显身手的好机会。
以之前的 jsonplaceholder.typicode.com/posts/1 接口为例,先模拟数据命名为post.json放在assert目录下,
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "local"
}
网络请求使用Dio框架,这里需要手动实现Dio、Response接口,但只实现需要的接口
class FakeResponse<T> implements Response<T> {
FakeResponse(this.data, {this.statusCode = 200});
@override
final T data;
@override
int? statusCode;
@override
void noSuchMethod(Invocation invocation) {
throw UnimplementedError();
}
}
class FakeDio implements Dio {
FakeDio();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
Future<Response<T>> get<T>(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) async {
switch (path) {
// 根据接口选择本地数据
case "https://jsonplaceholder.typicode.com/posts/1":
// 模拟延时
await Future.delayed(const Duration(seconds: 1));
// 加载之前准备好的本地数据,这里一般decode成Map,以便后续转Entity,这里简便起见直接返回字符串
String data = await rootBundle.loadString('assets/post.json');
return FakeResponse(data) as Response<T>;
}
throw UnimplementedError();
}
}
因为网络请求接口是全局的,所以在根布局里覆盖,当和后台正式调试时,去掉override即可,非常灵活
final dioProvider = Provider((ref) => Dio());
void main() {
runApp(ProviderScope(
overrides: [dioProvider.overrideWithValue(FakeDio())],// 本地调试使用
child: const OverrideApp()));
}
// 网络请求方式不变
final postProvider = FutureProvider<String>((ref) async {
Response<String> response = await ref
.watch(dioProvider) // 如果dioProvider被覆盖,获取的就是本地数据
.get<String>("https://jsonplaceholder.typicode.com/posts/1");
if (response.data == null || response.statusCode != 200) {
throw HttpException('Http Error! ${response.statusCode}');
} else {
return response.data!;
}
});
从此再也没有谁可以拦住端侧程序猿加班
override内部原理
在第一个Demo中,不知大家有没有好奇,不同的ItemView读取的都是同一个itemProvider,获取的值各不相同,这是怎么实现的,我们从ProviderScope入手,简单了解一下其内部流程。
ProviderScope是一个普通的StatefulWidget,他里面有一个重要容器ProvierContainer,是管理Providers的,这也是为什么Flutter中要想使用Provider,必须在ProviderScope限定下的原因,同时如果ProviderScope生命周期结束了,里面Providers对应的Element都要走dispose流程。
@override
void initState() {
super.initState();
// 找到上一个ProviderScope,把能继承的都搞过来
final scope = context
.getElementForInheritedWidgetOfExactType<UncontrolledProviderScope>()
?.widget as UncontrolledProviderScope?;
container = ProviderContainer(
parent: scope?.container,
overrides: widget.overrides,
observers: widget.observers,
);
}
这个ProvierContainer在initState时实例化,我们关心的事有两件
- 如果有父ProviderContainer,从它的_stateReaders集合中,把isDynamicallyCreated=false的stateReader继承下來,这个isDynamicallyCreated=false表示什么意思呢?看下面一条
- 遍历overrides集合,如果它是ProviderOverride类型,创建_StateReader放到Map中,不会等访问的时候再创建,和之前不一样的是这个isDynamicallyCreated=false,没有覆盖的这个属性都是true;而且origin和override是两个不同值,没有覆盖的是同一个值
class ProviderContainer {
ProviderContainer({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) :
...
_stateReaders = {
if (parent != null)
for (final entry in parent._stateReaders.entries)
// 筛选出被覆盖的Provider
if (!entry.value.isDynamicallyCreated) entry.key: entry.value,
},
_root = parent?._root ?? parent {
...
for (final override in overrides) {
if (override is ProviderOverride) {
_overrideForProvider[override._origin] = override._override;
_stateReaders[override._origin] = _StateReader(
origin: override._origin,
override: override._override,
container: this,
isDynamicallyCreated: false,
);
} else if (override is FamilyOverride) {
...
);
}
}
}
再看overrides集合里面是什么,以itemProvider为例,overrideWithValue方法返回的是ProviderOverride,其origin的值itemProvider充当的是一个标识符,真正提供值的是ValueProvider
ProviderScope(
overrides: [
itemProvider.overrideWithValue(numbers[index]),
],
child: item,
)
Override overrideWithValue(State value) {
return ProviderOverride(
origin: this,
override: ValueProvider<State>(value),
);
}
每个Item都属于一个ProviderScope,它们的_StateReader都是独立的,override值是各异的,但itemProvider是一样的,这是为什么说它是标识符的原因
搞清楚这个结构,后面的watch流程就简单了,简单说下重点
1.之前只有一个ProviderScope,毫无疑问,Provider都从它那取,现状可能有多个,找哪个?答案是离的最近的,这个依赖InheritedWidget实现,有兴趣可以看看之前的文章InheritedWidget
class ConsumerStatefulElement extends StatefulElement implements WidgetRef {
...
// 开始最重要的事情就是找到最近的ProviderScope,
late ProviderContainer _container = ProviderScope.containerOf(this);
}
- 获取_StateReader,前面初始化的时候已经放进去了,通过itemProvider直接取出用
_StateReader _getStateReader(ProviderBase provider) {
final currentReader = _stateReaders[provider];
if (currentReader != null) return currentReader;
...
}
3.初始化ProviderElement,现在回头再看这段代码就知道为什么用override去创建Element了,如果没有覆盖,override和origin的值是一样的,如果覆盖了override的优先级要高,这样就悄咪咪的完成了对原Provider的覆盖
ProviderElementBase _create() {
...
try {
final element = override.createElement()
.._provider = override
.._origin = origin
.._container = container
..mount();
...
return element;
} finally {
...
}
}
至此Riverpod1.0版本大部分内容应该都涉及到了,Riverpod的功能强大,尤其是和Hook结合起来,了解内部实现原理有助于写出更好的代码、摸更多的鱼