Riverpod之override(九)

806 阅读7分钟

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而不是传参

image.png

代码入口

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 --- 0878140213 --- 0
595695877 --- 11039269752 --- 1
595695877 --- 275365563 --- 2
595695877 --- 3995618539 --- 3
595695877 --- 4766272018 --- 4
595695877 --- 5979290826 --- 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!;
  }
});

从此再也没有谁可以拦住端侧程序猿加班

image.png

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是一样的,这是为什么说它是标识符的原因

image.png

搞清楚这个结构,后面的watch流程就简单了,简单说下重点

1.之前只有一个ProviderScope,毫无疑问,Provider都从它那取,现状可能有多个,找哪个?答案是离的最近的,这个依赖InheritedWidget实现,有兴趣可以看看之前的文章InheritedWidget

  class ConsumerStatefulElement extends StatefulElement implements WidgetRef {
    ...
    // 开始最重要的事情就是找到最近的ProviderScope,
  late ProviderContainer _container = ProviderScope.containerOf(this); 
    }
  1. 获取_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结合起来,了解内部实现原理有助于写出更好的代码、摸更多的鱼

webwxgetmsgimg (6).jpg