【Flutter脱发实录】拆轮子之Provider

1,385 阅读8分钟

关于Provider,网上的文章太多了,但都比较零散或者偏使用。觉得还是需要深入理解一下,这篇文章算是自己拆解Provider过程的一个记录吧。

Provider 原理解析

打开源码,最先注意到的就是Provider类。 此Provider非标题中的Provider。此处的Provider 顾名思义,只是一个数据的提供者,他只是对InheritedWidget的一层封装,方便我们对数据的操作。

看下源码~

居然,内容非常之少~

  • 继承自InheritedProvider
  • 需要指定一个泛型T
  • 一个普通构造器和一个命名构造器
  • 一个静态方法static T of<T>(BuildContext context, {bool listen = true}),返回指定泛型为T的对象

我们一条一条看。

要认识Provider,看其父类即可了解三分。InheritedProviderInheritedWidget有关系?源码注解上写的很直白:A generic implementation of an [InheritedWidget].果真,是InheritedWidget的一个通用实现。也就是说Provider是基于InheritedWidget原理实现的。对InheritedWidget不是很了解的,可以简单了解一下这篇【Flutter脱发实录】盘一盘InheritedWidget

因为InheritedWidget是从下往上,从Element链表中获取数据的,因此存在当前节点之上多个数据节点的情况。Provider是通过数据的runtimeType来区分不同的数据的。而泛型T就限制了数据的类型,以便能准确找到对应的数据。

我们直接贴上代码,逐一分析构造器中的参数。

// 创建数据 T 的Function 
typedef Create<T> = T Function(BuildContext context);
// 释放数据 T 的Fuction 可在父类中的State中看到 在State dispose中释放
typedef Dispose<T> = void Function(BuildContext context, T value);

typedef UpdateShouldNotify<T> = bool Function(T previous, T current);
Provider({
    Key key,
    @required Create<T> create,
    Dispose<T> dispose,
    bool lazy, 
    TransitionBuilder builder, 
    Widget child,
  })  : assert(create != null),
        super(
          key: key,
          lazy: lazy,
          builder: builder,
          create: create,
          dispose: dispose,
          debugCheckInvalidValueType: kReleaseMode
              ? null
              : (T value) =>
                  Provider.debugCheckInvalidValueType?.call<T>(value),
          child: child,
        );

类关系图

首先梳理一下类关系图,能帮助我们更好地学习 Provider关系类图

Provider左侧是InheritedWidget派系,负责处理数据在Element Tree中传递的逻辑。 Provider右侧是代理派系,主要负责value T 的生命周期管理。

一直没太搞明白一点,代理类分为_Delegate 和 _DelegateState类。_Delegate 的作用是什么,总感觉没用显得很多余?希望大佬点一下!!!谢谢

在构造器中有三个比较有意思的地方,单独分析一下其作用以及原理。

lazy懒加载

这里的lazy懒加载,指的是在创建Provider时,并不会去真正创建数据,即构造器中的Create方法,而是当第一次访问该数据时去创建。当数据的初始化逻辑比较复杂时,可以设置lazy = true来延迟加载。

原理非常简单,只有一处代码:

class _InheritedProviderScopeElement<T> extends InheritedElement
    implements InheritedContext<T> {

  @override
  Widget build() {
    if (widget.owner._lazy == false) {
      value; //  当非懒加载是   在build方法中  直接获取 value
    }
    ...
  }
}

当设置为非懒加载时,在构建时,即获取value对象,即执行value的生成逻辑,具体可见_CreateInheritedProviderState中的代码。 当设置为懒加载时,只会在value真实被获取时(Provider.of<T>()、context.read<T>()等),才会初始化。

builder 模式

builder模式的本质是一个语法糖,提供了一个可用的context参数,方便我们生成Widget

Provider.value()

当value很简单,无需关系它的生命周期时,我们可以使用Provider.value这个命名函数,更加便捷地创建Provider。

Provider.of <T> ()

Provider唯一暴露的静态方法,作用是向上查找最近的一个使用Provider植入的T类型的value,原理可参考InheritedWidget。

当无法找到对应的value时,会报ProviderNotFoundException异常

Provider 的变种

Provider部分实现的功能仅仅只是最基本的数据插入和获取,显然无法高效地实现业务功能,所以需要对Provider进一步的封装。 作者提供了如下几种变种,能满足大部分的使用场景。

ListenableProvider

继承自InheritedProvider类,做了两件事:

  1. 指定Listenable作为其泛型
  2. 默认实现了startListening参数

Listenable是Flutter提供的一个发布者-订阅者模式方案,具体可以自行百度,之后也会专门出一篇分析一下。

再看下startListening做了什么?

static VoidCallback _startListening(
    InheritedContext<Listenable> e,
    Listenable value,
  ) {
    value?.addListener(e.markNeedsNotifyDependents);
    return () => value?.removeListener(e.markNeedsNotifyDependents);
  }

静态方法提供了默认的实现

  1. 订阅事件,当接受到事件时,通知Element更新UI
  2. 返回一个默认取消订阅的回调

这个_startListening在哪里被使用了呢?在_CreateInheritedProviderState这个状态代理类中,在value创建时,注册此订阅事件,并记录回调

T get value {
    _removeListener ??= delegate.startListening?.call(element, _value);
}

在dispose方法中,调用回调取消此订阅

  @override
  void dispose() {
    super.dispose();
    _removeListener?.call();
    if (_didInitValue) {
      delegate.dispose?.call(element, _value);
    }
  }
ChangeNotifierProvider

我们最常用的一种变体,采用ChangeNotifier作为订阅模式。 当在ChangeNotifier类中,调用notifyListeners()即可通知UI更新。

ProxyProvider

对外暴露了一个update方法,可以帮助我们根据多个不同的value,更新当前持有的value

DeferredInheritedProvider

顾名思义,InheritedProvider的延迟版。子类主要通过创造不同的startListening (VoidCallback)来实现不同的延迟策略。 传入的startListening如下定义:

typedef DeferredStartListening<T, R> = VoidCallback Function(
  InheritedContext<R> context,
  void Function(R value) setState,
  T controller,
  R value,
);

由于存在延迟性,创建时并不能及时获取到value,于是提供了setState方法,此setState方法非State中的方法,当传入的异步controller准备完毕时,调用setState方法,更新value值并刷新UI

  void setState(R value) {
    if (_hasValue) {
      final shouldNotify = delegate.updateShouldNotify != null
          ? delegate.updateShouldNotify(_value, value)
          : _value != value;
      if (shouldNotify) {
        element.markNeedsNotifyDependents();
      }
    }
    _hasValue = true;
    _value = value;
  }
ValueListenableProvider

采用ValueListenable作为延迟controller。ValueListenable的特性就是关注其持有的value T,一单value发生变化,会自动调用发布订阅,调用addListener中注册的方法。

  static DeferredStartListening<ValueListenable<T>, T> _startListening<T>() {
    return (_, setState, controller, __) {
      setState(controller.value);

	  // 将setState方法添加到响应中
      final listener = () => setState(controller.value);
      controller.addListener(listener);
      return () => controller.removeListener(listener);
    };
  }
FutureProvider

使用Future作为延时策略。 需要传入一个T initialDataErrorBuilder<T> catchError错误回调。

DeferredStartListening<Future<T>, T> _futureStartListening<T>({
  T initialData,
  ErrorBuilder<T> catchError,
}) {
  return (e, setState, controller, __) {
    if (!e.hasValue) {
      setState(initialData);
    }

    var canceled = false;
    controller?.then(
      (value) {
        if (canceled) return;
        setState(value);
      },
      onError: (dynamic error) {
        if (canceled) return;
        if (catchError != null) {
          setState(catchError(e, error));
        }
      },
    );

    return () => canceled = true;
  };
}

首先使用初始化value,当Future执行完毕后,setState更新数据,发生异常时,使用catchError进行处理。

StreamProvider

使用Stream作为延时策略。 直接看startlistening的实现:

DeferredStartListening<Stream<T>, T> _streamStartListening<T>({
  T initialData,
  ErrorBuilder<T> catchError,
}) {
  return (e, setState, controller, __) {
    if (!e.hasValue) {
      setState(initialData);
    }
    if (controller == null) {
      return () {};
    }
    final sub = controller.listen(
      setState,
      onError: (dynamic error) {
        if (catchError != null) {
          setState(catchError(e, error));
        } 
      },
    );

    return sub.cancel;
  };
}

同样在Stream的listen方法中,调用setState方法。

Provider 优化刷新

由于直接使用Provider.of(BuildContext context)使用的context通常是较外层的,会导致不必要的刷新。Provider提供了两个Widget来优化页面的刷新逻辑:ConsumerSelector

Consumer

Consumer有两个直接的目的:

  1. 简化value的获取 - 替代Provider.of
  2. 提供细粒度的重构来优化页面刷新

这么重要的优化,代码居然如此之简单~

class Consumer<T> extends SingleChildStatelessWidget {
  Consumer({
    Key key,
    @required this.builder,
    Widget child,
  })  : assert(builder != null),
        super(key: key, child: child);
        
  final Widget Function(BuildContext context, T value, Widget child) builder;

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    return builder(
      context,
      Provider.of<T>(context),
      child,
    );
  }
}

仅仅只是定义了一个Builder方法就解决了! 看看builder做了哪些事?

  1. 使用新的context,保证只会刷新Consumer对应的Element之下的节点
  2. 内部实现对value的获取,直接返回了value
  3. 允许传入一个Widget作为child,当发生刷新时,child不会刷新。why???

Selector

使用Consumer时,当value中的一部分发生变化,即便是不关心的那部分,依然会发生重建。于是Selector出现了。 Selector 会摘出value中自己关心的那部分,只有当这部分发生改别时,才会重构。看下是如何实现的

class _Selector0State<T> extends SingleChildState<Selector0<T>> {
  T value;
  Widget cache;
  Widget oldWidget;

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    final selected = widget.selector(context);

    var shouldInvalidateCache = oldWidget != widget ||
        (widget._shouldRebuild != null &&
            widget._shouldRebuild.call(value, selected)) ||
        (widget._shouldRebuild == null &&
            !const DeepCollectionEquality().equals(value, selected));
    if (shouldInvalidateCache) {
      value = selected;
      oldWidget = widget;
      cache = widget.builder(
        context,
        selected,
        child,
      );
    }
    return cache;
  }
}

从源码中可以看到更新条件有如下几个:

  1. oldWidget != widget 这条就不多解释了
  2. widget._shouldRebuild != null && widget._shouldRebuild.call(value, selected))可以自己定义新老数据的比较方法
  3. widget._shouldRebuild == null && !const DeepCollectionEquality().equals(value, selected)默认会用DeepCollectionEquality类来进行比较

那么是如何保证不被重建的呢? Selector采用了缓存cache,当不需要更新时,直接将缓存的Widget返回。

BuildContext 扩展

在Provider中,存在4个对BuildContext的扩展

InheritedContext

InheritedContext是对基本功能的扩展,主要扩展了value方法的获取,最直接的体现就是Provider.of的实现方法。

  static T of<T>(BuildContext context, {bool listen = true}) {
    final inheritedElement = _inheritedElementOf<T>(context);

    if (listen) {
      context.dependOnInheritedElement(inheritedElement);
    }

    return inheritedElement.value;
  }

由于_InheritedProviderScopeElement实现了InheritedContext类,因此可以直接持有value。

SelectContext

通过传入一个selector,允许只关注value的部分内容,类似上文所述的Selector,但是实现原理缺失不同的,SelectContext是通过InheritedElement中的aspect实现的,具体可参考InheritedWidget

extension SelectContext on BuildContext {
  R select<T, R>(R selector(T value)) {
    final inheritedElement = Provider._inheritedElementOf<T>(this);
    try {
      var value = inheritedElement.value;
      final selected = selector(value);
      dependOnInheritedElement(
        inheritedElement,
        aspect: (T newValue) => !const DeepCollectionEquality()
            .equals(selector(newValue), selected),
      );
      return selected;
    } 
  }
}

ReadContext

默认了Provider.of<T>(this, listen: false),当数据变化时,不会发生重建

WatchContext

简化了Provider.of<T>(this)的写法

Provider大致就拆解完毕了,写的比较乱。关于具体的使用和Demo,网上有太多资源了,就不贴了。