👀

RiverpodのautoDispose深掘り

2023/03/12に公開

Riverpod、便利ですよね。毎日使っています。
そんなRiverpodには、.autoDisposeという便利な機能があります。

https://docs-v2.riverpod.dev/docs/concepts/modifiers/auto_dispose

この.autoDisposeは、効率的なアプリケーション開発を助けてくれます。
この仕組みについて、コードから理解を深めてみよう、というのが記事の趣旨です。


なお、この文章はriverpod: 2.3.1を参照しています。

https://github.com/rrousselGit/riverpod/tree/riverpod-v2.3.1

RiverpodとProviderContainer

手始めに、RiverpodにおけるProviderContainer破棄タイミングについて確認しましょう。なお、ここではFlutterにRiverpodを利用したケースを紹介します。
確認するべきクラスは次のとおりです。

クラス パッケージ 継承
ProviderScope flutter_riverpod extends StatefulWidget
ProviderScopeState flutter_riverpod extends State<ProviderScope>
ProviderContainer riverpod implements Node

ProviderScope

RiverpodをFlutterのアプリケーションに導入するには、ProviderScopeを利用します。大抵の場合は、サンプルコードにあるように、runAppの中で呼び出す形になるでしょう。

https://docs-v2.riverpod.dev/docs/getting_started

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

ProviderScopeStatefulWidgetを継承しているため、これはMyAppの親に1つStatefulWidgetを置いています。runAppの直下は、アプリケーションのrootとなるWidgetです。このため、起動から破棄までを管理できることになります。

ProviderScopeStatefulWidgetなため、固有のStateを持っています。それがProviderScopeStateです。そして、このProviderScopeStateのプロパティとして、ProviderContainerを保持しています。
下にコードを抜粋していますが、お時間があれば、ぜひ一度ソースコードを眺めてみてください。

class ProviderScopeState extends State<ProviderScope> {
  
  late final ProviderContainer container;

  
  void initState() {
    super.initState();

    final parent = _getParent();
    container = ProviderContainer(
      parent: parent,
      overrides: widget.overrides,
      observers: widget.observers,
    );
  }

  
  Widget build(BuildContext context) {
    return UncontrolledProviderScope(
      container: container,
      child: widget.child,
    );
  }

  
  void dispose() {
    container.dispose();
    super.dispose();
  }
}

ここで登場するUncontrolledProviderScopeInheritedWidgetを継承したクラスで、この要素に対してProviderContainerを提供する役割を持っています。実装を確認すると細々とした調整を行っているようなのですが、今回はスキップします。
ProviderScopeにより、アプリケーションの起動時に用意され破棄時にdisposeされるProviderContainerが提供されることがわかりました。では、このProviderContainerをどのようにWidgetが呼び出しているのかを、先に確認します。

RiverpodとConsumerStatefulWidget

RiverpodをFlutterで利用するためには、ConsumerConsumerWidgetConsumerStatefulWidgetのいずれかを利用します。

クラス パッケージ 継承
ConsumerWidget flutter_riverpod extends ConsumerStatefulWidget
ConsumerStatefulWidget flutter_riverpod extends StatefulWidget
ConsumerStatefulElement flutter_riverpod extends StatefulElement implements WidgetRef
ProviderContainer riverpod implements Node

ConsumerWidget

ConsumerConsumerWidgetを継承し、builderによる子要素の生成を提供するクラスです。コードを確認すると簡単にわかるのですが、ConsumerConsumerWidgetの間にはほとんど差がありません。StatefulWidgegtの内部でProviderを参照したいケースや、childプロパティを利用したパフォーマンスの最適化を行う場合、ConsumerWidgetではなくConsumerを使うべきとされています。
ConsumerWidgetConsumerStatefulWidgetを継承したクラスです。実態としてはStatefulWidgetの継承クラスになりますが、API的にStatelessWidgetのように利用できるよう、調整されています。

APIの関係性を把握するため、必要な箇所だけ抜粋すると、次のような処理になっています。

abstract class ConsumerWidget extends ConsumerStatefulWidget {
  const ConsumerWidget({super.key});

  Widget build(BuildContext context, WidgetRef ref);

  
  _ConsumerState createState() => _ConsumerState();
}

class _ConsumerState extends ConsumerState<ConsumerWidget> {
  
  WidgetRef get ref => context as WidgetRef;

  
  Widget build(BuildContext context) {
    return widget.build(context, ref);
  }
}

abstract class ConsumerState<T extends ConsumerStatefulWidget> extends State<T> {
  late final WidgetRef ref = context as WidgetRef;
}

サッと見てわかる通り、重要な実装は全てConsumerStatefulWidgetにまとまっています。

ConsumerStatefulWidget

ConsumerStatefulWidgetStatefulWidgetを継承したクラスです。StateとしてConsumerStateを、そしてStatefulElementとしてConsumerStatefulElementを返します。
このうち、Riverpod的に重要なのはConsumerStatefulElementです。

下にコードを記載します。
処理に影響がない範囲でコードの省略や修正をしていますので、ぜひお時間のある時に元のコードをご確認ください。

class ConsumerStatefulElement extends StatefulElement implements WidgetRef {
  ConsumerStatefulElement(ConsumerStatefulWidget super.widget);

  late ProviderContainer _container = ProviderScope.containerOf(this);
  var _dependencies = <ProviderListenable<Object?>, ProviderSubscription<Object?>>{};
  Map<ProviderListenable<Object?>, ProviderSubscription<Object?>>? _oldDependencies;

  
  Widget build() {
    try {
      _oldDependencies = _dependencies;
      _dependencies = {};
      return super.build();
    } finally {
      for (final dep in _oldDependencies!.values) {
        dep.close();
      }
      _oldDependencies = null;
    }
  }

  
  void unmount() {
    super.unmount();

    for (final dependency in _dependencies.values) {
      dependency.close();
    }
  }

  
  T watch<T>(ProviderListenable<T> target) {
    return _dependencies.putIfAbsent(target, () {
      final oldDependency = _oldDependencies?.remove(target);

      if (oldDependency != null) {
        return oldDependency;
      }

      return _container.listen<T>(
        target,
        (_, __) => markNeedsBuild(),
      );
    }).read() as T;
  }

  
  T read<T>(ProviderListenable<T> provider) {
    return ProviderScope.containerOf(this, listen: false).read(provider);
  }

  
  State refresh<State>(Refreshable<State> provider) {
    return ProviderScope.containerOf(this, listen: false).refresh(provider);
  }

  
  void invalidate(ProviderOrFamily provider) {
    _container.invalidate(provider);
  }

  
  BuildContext get context => this;
}

ProviderScope.containerOfが頻繁に呼び出されているのがわかりますね。これはcontextから参照できるInhertedWidgetを参照し、ProviderContainerを取得する関数です。先ほど確認した通り、大抵の場合はアプリケーションのrootにProviderScopeを置いているため、アプリケーション全体で利用できるProviderContainerを参照することになります。
StatefulElementを継承しているクラスでは、_dependencies(ref.watchを呼び出したもの)と_listeners(ref.listenを呼び出したもの)の更新をしています。

Riverpodの動きを把握するためには、watchbuild、そしてunmountを確認すれば大丈夫です。


Widget build() {
  try {
    _oldDependencies = _dependencies;
    _dependencies = {};
    return super.build();
  } finally {
    for (final dep in _oldDependencies!.values) {
      dep.close();
    }
    _oldDependencies = null;
  }
}


void unmount() {
  super.unmount();

  for (final dependency in _dependencies.values) {
    dependency.close();
  }
}


T watch<T>(ProviderListenable<T> target) {
  return _dependencies.putIfAbsent(target, () {
    final oldDependency = _oldDependencies?.remove(target);

    if (oldDependency != null) {
      return oldDependency;
    }

    return _container.listen<T>(
      target,
      (_, __) => markNeedsBuild(),
    );
  }).read() as T;
}

buildの処理は一見わかりにくいのですが、ConsumerStatefulWidgetを継承したクラスでsuper.buildを呼び出すことはほぼないため、実行されることは稀です。unmountElementのライフサイクルに詳細があるのですが、非アクティブになってから所定の時間後に遷移する状態です。一度unmountになったElementは、再度Elementのtreeに追加されることがないため、破棄されることが決定ずれられたタイミングであると言えます。

ProviderContainerdispose

ここまでにみてきた処理は、基本的な呼び出しと破棄の処理です。

ProviderはProviderScope内のProviderContainerに紐づいています。最も基本的なConsumerStatefulWidgetのコードを見ると、readwatchともにProviderScope.containerOfを利用した取得であることが確認できます。
またConsumerStatefulWidgetunmount時、ref.watchで参照したProviderSubscriptioncloseされます。これはProviderContainerlisten関数の呼び出しであり、stateの変更時に呼び出されるコールバックです。

https://pub.dev/documentation/riverpod/2.3.1/riverpod/ProviderContainer/listen.html

RiverpodとProvider

ここからは、Providerについて確認していきます。

ProviderにはProviderの他に、FutureProviderNotifierProviderなどが存在します。今回は「とりあえず処理を追いたい」ので、Providerを中心に確認します。

クラス パッケージ 継承
Provider riverpod extends InternalProvider<T> with AlwaysAliveProviderBase<T>
InternalProvider riverpod extends ProviderBase<T> with OverrideWithValueMixin<T>
ProviderBase riverpod extends ProviderOrFamily with ProviderListenable<T> implements ProviderOverride, Refreshable<T>
ProviderElementBase riverpod implements Ref<T>, Node
AutoDisposeProvider riverpod extends InternalProvider<T>
AutoDisposeProviderElement riverpod extends ProviderElement<T> with AutoDisposeProviderElementMixin<T> implements AutoDisposeProviderRef<T>
AutoDisposeProviderElementMixin riverpod on ProviderElementBase<T> implements AutoDisposeRef<T>

Provider

Providerはいくつものクラスを継承、mixinされることで成り立っています。今回は、実装の中心となるProviderBaseを確認します。
ProviderBaseは一見すると読みにくいのですが、ref.readref.watchで呼び出される関数を確認することで、割合簡単に把握することができます。以下、把握用にコードを抜粋します。


abstract class ProviderBase<T> extends ProviderOrFamily with ProviderListenable<T> implements ProviderOverride, Refreshable<T> {
  
  ProviderSubscription<T> addListener(
    Node node,
    void Function(T? previous, T next) listener, {
    required void Function(Object error, StackTrace stackTrace)? onError,
    required void Function()? onDependencyMayHaveChanged,
    required bool fireImmediately,
  }) {
    onError ??= Zone.current.handleUncaughtError;

    final element = node.readProviderElement(this);

    element.flush();
    if (fireImmediately) {
      handleFireImmediately(
        element.getState()!,
        listener: listener,
        onError: onError,
      );
    }

    element._onListen();

    return node._listenElement(
      element,
      listener: listener,
      onError: onError,
    );
  }

  
  State read(Node node) {
    final element = node.readProviderElement(this);

    element.flush();

    // In case `read` was called on a provider that has no listener
    element.mayNeedDispose();

    return element.requireState;
  }
}

ref.read

readにはNodeが登場します。ProviderContainerNodeをimplemntしていることと、ConsumerStatefulWidgetProviderScope.containerOfを呼び出していたことを思い出せば、処理の流れは把握できますね。

class ConsumerStatefulElement extends StatefulElement implements WidgetRef {
  
  T read<T>(ProviderListenable<T> provider) {
    return ProviderScope.containerOf(this, listen: false).read(provider);
  }
}

class ProviderScope extends StatefulWidget {
  /// Read the current [ProviderContainer] for a [BuildContext].
  static ProviderContainer containerOf(
    BuildContext context, {
    bool listen = true,
  }) {
    UncontrolledProviderScope? scope;

    if (listen) {
      scope = context //
          .dependOnInheritedWidgetOfExactType<UncontrolledProviderScope>();
    } else {
      scope = context
          .getElementForInheritedWidgetOfExactType<UncontrolledProviderScope>()
          ?.widget as UncontrolledProviderScope?;
    }

    if (scope == null) {
      throw StateError('No ProviderScope found');
    }

    return scope.container;
  }
}

class ProviderContainer implements Node {
  Result read<Result>(
    ProviderListenable<Result> provider,
  ) {
    return provider.read(this);
  }
}


abstract class ProviderBase<T> extends ProviderOrFamily
    with ProviderListenable<T>
    implements ProviderOverride, Refreshable<T> {

  
  State read(Node node) {
    final element = node.readProviderElement(this);

    element.flush();

    // In case `read` was called on a provider that has no listener
    element.mayNeedDispose();

    return element.requireState;
  }
}

ProviderContainerreadが「引数として渡されたproviderread関数を呼び出している」ため混乱するかもしれません。順を追って読んでいくと、Nodeに対してProviderBase<T>を渡していることが把握できます。
readProviderElementの実装は、NodeをimplementしているProviderContainerにあります。

class ProviderContainer implements Node {
  
  ProviderElementBase<T> readProviderElement<T>(
    ProviderBase<T> provider,
  ) {
    if (_disposed) {
      throw StateError(
        'Tried to read a provider from a ProviderContainer that was already disposed',
      );
    }

    final reader = _getStateReader(provider);

    return reader.getElement() as ProviderElementBase<T>;
  }
}

_getStateReader(provider)の内容を短くまとめるのは難しいのですが、おおよそ「providerに対応する_StateReaderを生成/キャッシュ」している関数だと言えます。そして_StateReaderProviderElementBaseを生成/キャッシュするクラスです。
ProviderElementBaseは正直追いかけるのが難しいクラスなので、下記のコードをざっと眺め、Result<T>?を管理していることを把握してみてください。

/// An internal class that handles the state of a provider.
///
/// Do not use.
abstract class ProviderElementBase<T> implements Ref<T>, Node {
  /// Do not use.
  ProviderElementBase(this._provider);

  /* STATE */
  Result<T>? _state;

  /// Update the exposed value of a provider and notify its listeners.
  ///
  /// Listeners will only be notified if [updateShouldNotify]
  /// returns true.
  ///
  /// This API is not meant for public consumption. Instead if a [Ref] needs
  /// to expose a way to update the state, the practice is to expose a getter/setter.
  
  void setState(T newState) {
    final previousResult = getState();
    final result = _state = ResultData(newState);

    if (_didBuild) {
      _notifyListeners(result, previousResult);
    }
  }

  /// Obtains the current state, of null if the provider has yet to initialize.
  ///
  /// The returned object will contain error information, if any.
  /// This function does not cause the provider to rebuild if it someohow was
  /// outdated.
  ///
  /// This is not meant for public consumption. Instead, public API should use
  /// [readSelf].
  
  
  Result<T>? getState() => _state;

  /// A utility for re-initializing a provider when needed.
  ///
  /// Calling [flush] will only re-initialize the provider if it needs to rerun.
  /// This can involve:
  /// - a previous call to [invalidateSelf]
  /// - a dependency of the provider has changed (such as when using [watch]).
  ///
  /// This is not meant for public consumption. Public API should hide
  /// [flush] from users, such that they don't need to care about invocing this function.
  void flush() {
    _maybeRebuildDependencies();
    if (_mustRecomputeState) {
      _mustRecomputeState = false;
      _performBuild();
    }
  }
}

ProviderではProviderElementBaseを継承したProviderElementを利用します。

class ProviderElement<T> extends ProviderElementBase<T> implements ProviderRef<T> {
  /// A [ProviderElementBase] for [Provider]
  ProviderElement._(super.provider);

  
  T get state => requireState;

  
  set state(T newState) => setState(newState);

  
  void create({required bool didChangeDependency}) {
    final provider = this.provider as InternalProvider<T>;

    setState(provider._create(this));
  }

  
  bool updateShouldNotify(T previous, T next) {
    return previous != next;
  }
}

ProviderElementcreateは、mount()flush()の中で呼び出されます。mount()_StateReadergetElement()が初めて呼ばれるタイミング、flush()は(ここでは)ProviderBasereadが呼ばれるタイミングです。これで、ref.readの呼び出しがされた際にProviderの引数として与えた処理が実行されるタイミングがわかりました!

ref.watch

続いて、ref.watchのケースも確認しましょう。readのケースで見慣れたクラスが多くなるため、ここまで読み進めた方ならば、そこまで苦労はないはずです。

class ConsumerStatefulElement extends StatefulElement implements WidgetRef {
  late ProviderContainer _container = ProviderScope.containerOf(this);
  
  
  Res watch<Res>(ProviderListenable<Res> target) {
    return _dependencies.putIfAbsent(target, () {
      final oldDependency = _oldDependencies?.remove(target);

      if (oldDependency != null) {
        return oldDependency;
      }

      return _container.listen<Res>(
        target,
        (_, __) => markNeedsBuild(),
      );
    }).read() as Res;
  }
}

class ProviderContainer implements Node {
  
  ProviderSubscription<T> listen<T>(
    ProviderListenable<T> provider,
    void Function(T? previous, T next) listener, {
    bool fireImmediately = false,
    void Function(Object error, StackTrace stackTrace)? onError,
  }) {
    return provider.addListener(
      this,
      listener,
      fireImmediately: fireImmediately,
      onError: onError,
      onDependencyMayHaveChanged: null,
    );
  }

  
  ProviderSubscription<T> _listenElement<T>(
    ProviderElementBase<T> element, {
    required void Function(T? previous, T next) listener,
    required void Function(Object error, StackTrace stackTrace) onError,
  }) {
    final sub = _ExternalProviderSubscription<T>._(
      element,
      listener,
      onError: onError,
    );

    element._externalDependents.add(sub);

    return sub;
  }
}

abstract class ProviderBase<T> extends ProviderOrFamily with ProviderListenable<T> implements ProviderOverride, Refreshable<T> {
  
  ProviderSubscription<T> addListener(
    Node node,
    void Function(T? previous, T next) listener, {
    required void Function(Object error, StackTrace stackTrace)? onError,
    required void Function()? onDependencyMayHaveChanged,
    required bool fireImmediately,
  }) {
    onError ??= Zone.current.handleUncaughtError;

    final element = node.readProviderElement(this);

    element.flush();
    if (fireImmediately) {
      handleFireImmediately(
        element.getState()!,
        listener: listener,
        onError: onError,
      );
    }

    element._onListen();

    return node._listenElement(
      element,
      listener: listener,
      onError: onError,
    );
  }
}

abstract class ProviderElementBase<T> implements Ref<T>, Node {
  void _onListen() {
    _onAddListeners?.forEach(runGuarded);
    if (_didCancelOnce && !hasListeners) {
      _onResumeListeners?.forEach(runGuarded);
    }
  }
}

/// Represents the subscription to a [ProviderListenable]
abstract class ProviderSubscription<T> {
  /// Stops listening to the provider
  
  void close();

  /// Obtain the latest value emitted by the provider
  T read();
}

class _ExternalProviderSubscription<T> implements ProviderSubscription<T> {
  /// 省略
}

ref.watchref.readと同じようにProviderElementBaseResult<T>?を保持しつつ、変更があった場合にコールバックを受け取るような実装になっています。ProviderElementBase._onListenの詳細については、次のAutoDisposeProviderで確認していくので、一旦眺める程度としてみてください。
ConsumerStatefulElementでは、listenerが動くたびにmarkNeedsBuild()をセットしています。この処理により、ref.watchで監視しているReulstに変更があったとき、ConsumerStatefulWidgetを継承しているクラスのbuild関数が呼び出されるようになります。

AutoDisposeProvider

続いて、.autoDisposeをつけたケースを確認しましょう。Provider.autoDisposeを呼び出すか、直接AutoDisposeProviderを呼び出したケースとなります。
何もないProviderと比べると、所々でAutoDispose用のクラスに投げ分けられていることがわかります。

class AutoDisposeProvider<T> extends InternalProvider<T> {
  final T Function(AutoDisposeProviderRef<T> ref) _createFn;

  
  T _create(AutoDisposeProviderElement<T> ref) => _createFn(ref);

  
  AutoDisposeProviderElement<T> createElement() {
    return AutoDisposeProviderElement._(this);
  }
}

class Provider<T> extends InternalProvider<T> with AlwaysAliveProviderBase<T> {
  final Create<T, ProviderRef<T>> _createFn;

  
  T _create(ProviderElement<State> ref) => _createFn(ref);

  
  ProviderElement<State> createElement() => ProviderElement._(this);
}

クラス的にはAutoDisposeProviderElementが処理をしていそうなのですが、AutoDisposeの処理はAutoDisposeProviderElementMixinに分割されています。
コメントやdeprecatedな処理を除いたコードを記載します。

abstract class ProviderRef<T> implements Ref<T> {
  T get state;
  set state(T newState);
}

abstract class AutoDisposeProviderRef<T> 
    extends ProviderRef<T> 
    implements AutoDisposeRef<T> {}

class AutoDisposeProviderElement<T> 
    extends ProviderElement<T> 
    with AutoDisposeProviderElementMixin<T> 
    implements AutoDisposeProviderRef<T> {
  AutoDisposeProviderElement._(AutoDisposeProvider<T> super.provider) : super._();
}

/// A mixin that adds auto dispose support to a [ProviderElementBase].
mixin AutoDisposeProviderElementMixin<T> on ProviderElementBase<T> implements AutoDisposeRef<T> {
  List<KeepAliveLink>? _keepAliveLinks;

  
  KeepAliveLink keepAlive() {
    final links = _keepAliveLinks ??= [];

    late KeepAliveLink link;
    link = KeepAliveLink._(() {
      if (links.remove(link)) {
        if (links.isEmpty) mayNeedDispose();
      }
    });
    links.add(link);

    return link;
  }

  
  void mayNeedDispose() {
    final links = _keepAliveLinks;

    if (!hasListeners && (links == null || links.isEmpty)) {
      _container._scheduler.scheduleProviderDispose(this);
    }
  }

  
  void runOnDispose() {
    _keepAliveLinks?.clear();
    super.runOnDispose();
  }
}

/// A object which maintains a provider alive
class KeepAliveLink {
  KeepAliveLink._(this._close);

  final void Function() _close;

  /// Release this [KeepAliveLink], allowing the associated provider to
  /// be disposed if the provider is no-longer listener nor has any
  /// remaining [KeepAliveLink].
  void close() => _close();
}

AutoDisposeProviderElementMixinmayNeedDisposeをoverrideしています。直感的に理解できるように、この処理は「keepAliveされていない場合、mayNeedDisposeが呼び出された際にdispose()する」処理です。
また、runOnDisposeProviderElementBasedispose()、もしくはinvalidateSelf()が呼び出されたタイミングで実行されます。前者は状態を保持しているProviderElementBaseが破棄されるタイミング。後者は開発者が「状態の再取得をしたい」と処理を行ったタイミングです。

ref.read

ref.readの一連の処理の中で、都度mayNeedDisposeが呼び出されています。このため、都度disposeが行われるます。
ただAutoDisposeProviderが別の箇所でref.watchされている場合、都度disposeされる処理がスキップされることがあります。

abstract class ProviderElementBase<State> implements Ref<State>, Node {
  /// Whether this [ProviderElementBase] is currently listened to or not.
  ///
  /// This maps to listeners added with [listen].
  /// See also [mayNeedDispose], called when [hasListeners] may have changed.
  bool get hasListeners =>
      _externalDependents.isNotEmpty ||
      _subscribers.isNotEmpty ||
      _providerDependents.isNotEmpty;

  /// The list of [ProviderSubscription]s that are linked with this element,
  /// which aren't coming from another provider.
  ///
  /// This is typically Flutter widgets or manual calls to [ProviderContainer.listen]
  /// with this provider as target.
  final _externalDependents = <_ExternalProviderSubscription<State>>[];
}

class ProviderContainer implements Node {
  
  ProviderSubscription<T> _listenElement<T>(
    ProviderElementBase<T> element, {
    required void Function(T? previous, T next) listener,
    required void Function(Object error, StackTrace stackTrace) onError,
  }) {
    final sub = _ExternalProviderSubscription<T>._(
      element,
      listener,
      onError: onError,
    );

    element._externalDependents.add(sub);

    return sub;
  }
}

mixin AutoDisposeProviderElementMixin<T> on ProviderElementBase<T> implements AutoDisposeRef<T> {
  
  void mayNeedDispose() {
    final links = _keepAliveLinks;

    if (!hasListeners && (links == null || links.isEmpty)) {
      _container._scheduler.scheduleProviderDispose(this);
    }
  }
}

ref.watch

ProviderElementBase.flush()が呼び出されたタイミングで、ProviderElementBase._mustRecomputeStatetrueであるとき、mayNeedDisposeが呼び出されます。
これはProviderElementBase.invalidateSelf()による更新フラグが立っているケース、もしくはProviderElementBasewatchしているProviderListenableが更新されたケースです。後者のケースは、詳しい方がいればご指摘ただければと思うのですが、筆者の(2023年3月の)理解では「Providerの中で、ほかのProviderを呼び出している」ケースを指しています。というのも、ProviderElementBaseRefとして振る舞うのは、典型的にはProvider_create関数だからです。


ref.readと異なり、ref.watchConsumerStatefulWidgetbuild関数内で呼び出されます。この呼び出しに関しては、これまでに確認しkてきた通り、ConsumerStatefulElementunmountが破棄の契機となります。
仕組みとしては、ref.watchをするとConsumerStatefulElement_dependenciesProviderSubscriptionが追加されます。Widgetが利用されなくなりunmountが呼ばれると、この_dependenciesに保持された全てのProviderSubscriptionclose()され、連鎖的にmayNeedDisposeが呼び出されるようになっています。

class ConsumerStatefulElement extends StatefulElement implements WidgetRef {
  
  Res watch<Res>(ProviderListenable<Res> target) {
    return _dependencies.putIfAbsent(target, () {
      final oldDependency = _oldDependencies?.remove(target);

      if (oldDependency != null) {
        return oldDependency;
      }

      return _container.listen<Res>(
        target,
        (_, __) => markNeedsBuild(),
      );
    }).read() as Res;
  }

  
  void unmount() {
    super.unmount();

    for (final dependency in _dependencies.values) {
      dependency.close();
    }
  }
}

class _ExternalProviderSubscription<T> implements ProviderSubscription<T> {
  
  void close() {
    _closed = true;
    _listenedElement._externalDependents.remove(this);
    _listenedElement._onRemoveListener();
  }
}

abstract class ProviderElementBase<State> implements Ref<State>, Node {
  void _onRemoveListener() {
    _onRemoveListeners?.forEach(runGuarded);
    if (!hasListeners) {
      _didCancelOnce = true;
      _onCancelListeners?.forEach(runGuarded);
    }
    mayNeedDispose();
  }
}

「Elementがunmountになった際」に破棄されるので、ドキュメントでは「Providerが参照されなくなった際」と表現されています。
ここまで確認された方ならば、この文章を納得を持って受け入れられるはずです。

mayNeedDisposekeepAlive

最後に、.keepAliveについて確認します。

https://docs-v2.riverpod.dev/docs/concepts/modifiers/auto_dispose#refkeepalive

コードを見るとわかる通り、.keepAliveを呼び出すと、mayNeedDisposedispose処理をスキップするようになります。この処理は、すでにdeprecatedになっているmaintainStateと同じです。
keepAlivemaintainStateより優れている点は、ref.keepAliveの返り値を利用することで、呼び出し側が任意のタイミングでmayNeedDisposeを呼び出せるようになった点です。筆者はそこまでユースケースを思いつかないのですが、この変更により、より適切に処理を閉じれるようになるケースも存在するのではと思います。

サンプルコードにあるように、.keepAliveを呼び出すタイミングは任意となります。
このため、処理の失敗時には.keepAliveをつけないが成功時にはつける、といった調整を行うことで、成功したケースのみを保持することもできます。


AutoDisposeProvider+keepAliveProviderは、ともにProviderContainerと同時にdisposeされる点で、ほぼ同じであると言えます。唯一違うのは、Providerの中でProviderを呼び出す際に、AutoDisposeするProviderの中ではAutoDisposeされるProviderを呼び出す必要がある、という点です。
AutoDisposeされるProviderを重ねることで、ProviderContainerの中に不要なProviderを積み上げなくて良くなります。そこまで問題になるケースがないような気がしますが、.autoDisposeを活用することで、より軽快なアプリケーションが実現できるかもしれません。

おわりに

ここまでお読みいただき、ありがとうございました。単にRiverpodの.autoDisposeありなし、そして.keepAliveの振る舞いについて確認したかっただけなのですが、非常に長い記事となってしまいました。まさかという感じです。

Riverpodは、非常に良くできたライブラリだと思います。
どんどん理解を深めて、より楽しく、快適なFlutterライフを送りましょう!

GitHubで編集を提案

Discussion