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?.