Riverpod autoDispose Deep Dive

Created At: 2023-09-10 07:46:31 Updated At: 2023-09-10 16:11:21

This .autoDispose helps in efficient application development in Flutter in Riverpod. The purpose of this article is to deepen your understanding of this mechanism from the code.

Riverpod and ProviderContainer

First, let's check the timing of destruction ProviderContainer in Riverpod. In addition, here we will introduce a case where Riverpod is used with Flutter. The classes you should check are:

ProviderScope

To introduce Riverpod into your Flutter application, use ProviderScope .  In most cases, you will call runApp inside the code, as shown in the sample code.

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

Since ProviderScope inherits from the StatefulWidget, this puts one StatefulWidget in the parent of MyApp. Directly under runApp is the widget that serves as the root of the application. This allows you to manage everything from startup to destruction.


Since ProviderScope is a StatefulWidget, it has its own State. That is the ProviderScopeState. And as a property of this ProviderScopeState, we hold the ProviderContainer.


The code is excerpted below, but if you have time, please take a look at the source code. ProviderContainer

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

  @override
  void initState() {
    super.initState();

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

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

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

The UncontrolledProvidersCope that appears here is a class that inherits the inheritedwidget, which has the role of providing ProviderContainer for this element.


When I check the implementation, it seems to be making detailed adjustments, but this time I skip it.
ProvidersCope has shown that ProviderContainer, which is prepared when the application is launched and discarded, is provided.


Now, check how Widget is calling this ProviderContainer.

Riverpod and ConsumerStatefulWidget

To use Riverpod with Flutter, use one of the following .Consumer ConsumerWidget ConsumerStatefulWidget

ConsumerWidget

Consumer is a class that inherits ConsumerWidget and provides generation of child elements using a builder. As you can easily see by looking at the code, there is almost no difference between Consumer and ConsumerWidget. If you want to reference the Provider inside a StatefulWidgegt or optimize performance using the child property, you should use Consumer instead of ConsumerWidget.


ConsumerWidget is a class that inherits ConsumerStatefulWidget. In actuality, it is an inherited class of StatefulWidget, but the API has been adjusted so that it can be used like StatelessWidget.

In order to understand the relationship between the APIs, we have excerpted only the necessary parts, and the processing is as follows.

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

  Widget build(BuildContext context, WidgetRef ref);

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

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

  @override
  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 is a class that inherits from StatefulWidget. Returns ConsumerState as State and ConsumerStatefulElement as StatefulElement.
Among these, ConsumerStatefulElement is important for Riverpod.

The code is below.
We have omitted and modified the code to the extent that it does not affect processing, so please take a look at the original code when you have time.

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;

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

  @override
  void unmount() {
    super.unmount();

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

  @override
  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;
  }

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

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

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

  @override
  BuildContext get context => this;
}

You can see that ProviderScope.containerOf is called frequently. This is a function that refers to the InhertedWidget that can be referenced from the context and obtains the ProviderContainer. As we confirmed earlier, in most cases, the ProviderScope is placed at the root of the application, so it refers to the ProviderContainer that can be used throughout the application.


In the class that inherits from StatefulElement, _dependencies (those that call ref.watch) and _listeners (those that call ref.listen) are updated.

To understand the movement of Riverpod, you can check watch, build, and unmount.

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

@override
void unmount() {
  super.unmount();

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

@override
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;
}

The build process is difficult to understand at first glance, but super.build is almost never called in a class that inherits ConsumerStatefulWidget, so it is rarely executed. Unmount has details in the life cycle of an Element, but it is a state that it transitions to after a predetermined period of time after it becomes inactive. Once an Element is unmounted, it is never added to the Element tree again, so it can be said that the decision to discard it has been delayed.

ProviderContainer dispose

The processing we have seen so far is basic invocation and disposal processing.

Provider is linked to ProviderContainer in ProviderScope. If you look at the most basic ConsumerStatefulWidget code, you can see that both read and watch are obtained using ProviderScope.containerOf.


Also, when the ConsumerStatefulWidget is unmounted, the ProviderSubscription referenced by ref.watch will be closed. This is a call to the ProviderContainer's listen function and is a callback that is called when the state changes.

Riverpod and Provider

From here, we will check about the Provider.

In addition to Provider, there are other types of Provider such as FutureProvider and NotifierProvider. This time, I want to follow the process, so I will mainly check the Provider.

Provider
Provider consists of inheriting and mixing several classes. This time, we will check ProviderBase, which is the core of the implementation.

ProviderBase is difficult to read at first glance, but you can easily understand the proportions by checking the functions called in ref.read and ref.watch. Below is an excerpt of the code for your understanding.

@immutable
abstract class ProviderBase<T> extends ProviderOrFamily with ProviderListenable<T> implements ProviderOverride, Refreshable<T> {
  @override
  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,
    );
  }

  @override
  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

Node appears in ref.read read. If you remember that ProviderContainer implements Node and that ConsumerStatefulWidget calls ProviderScope.containerOf, you can understand the process flow.

class ConsumerStatefulElement extends StatefulElement implements WidgetRef {
  @override
  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);
  }
}

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

  @override
  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;
  }
}

It may be confusing because the ProviderContainer's read is "calling the read function of the provider passed as an argument".

If you read it step by step, you will understand that the ProviderBase is passed to the Node.
The implementation of readProviderElement is in the ProviderContainer that implements Node.

class ProviderContainer implements Node {
  @override
  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>;
  }
}

It is difficult to summarize the contents of _getStateReader(provider) briefly, but it can be said that it is a function that ``generates/caches _StateReader corresponding to provider.''

And _StateReader is the class that generates/caches the ProviderElementBase.

To be honest, ProviderElementBase is a difficult class to follow, so take a quick look at the code below and try to understand that it manages Result?.

Comment

Add Reviews