1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'container.dart';
6/// @docImport 'restoration_properties.dart';
7library;
8
9import 'package:flutter/foundation.dart';
10import 'package:flutter/rendering.dart';
11import 'package:flutter/services.dart';
12
13import 'basic.dart';
14import 'framework.dart';
15
16export 'package:flutter/services.dart' show RestorationBucket;
17
18/// Creates a new scope for restoration IDs used by descendant widgets to claim
19/// [RestorationBucket]s.
20///
21/// {@template flutter.widgets.RestorationScope}
22/// A restoration scope inserts a [RestorationBucket] into the widget tree,
23/// which descendant widgets can access via [RestorationScope.of]. It is
24/// uncommon for descendants to directly store data in this bucket. Instead,
25/// descendant widgets should consider storing their own restoration data in a
26/// child bucket claimed with [RestorationBucket.claimChild] from the bucket
27/// provided by this scope.
28/// {@endtemplate}
29///
30/// The bucket inserted into the widget tree by this scope has been claimed from
31/// the surrounding [RestorationScope] using the provided [restorationId]. If
32/// the [RestorationScope] is moved to a different part of the widget tree under
33/// a different [RestorationScope], the bucket owned by this scope with all its
34/// children and the data contained in them is moved to the new scope as well.
35///
36/// This widget will not make a [RestorationBucket] available to descendants if
37/// [restorationId] is null or when there is no surrounding restoration scope to
38/// claim a bucket from. In this case, descendant widgets invoking
39/// [RestorationScope.of] will receive null as a return value indicating that no
40/// bucket is available for storing restoration data. This will turn off state
41/// restoration for the widget subtree.
42///
43/// See also:
44///
45/// * [RootRestorationScope], which inserts the root bucket provided by
46/// the [RestorationManager] into the widget tree and makes it accessible
47/// for descendants via [RestorationScope.of].
48/// * [UnmanagedRestorationScope], which inserts a provided [RestorationBucket]
49/// into the widget tree and makes it accessible for descendants via
50/// [RestorationScope.of].
51/// * [RestorationMixin], which may be used in [State] objects to manage the
52/// restoration data of a [StatefulWidget] instead of manually interacting
53/// with [RestorationScope]s and [RestorationBucket]s.
54/// * [RestorationManager], which describes the basic concepts of state
55/// restoration in Flutter.
56class RestorationScope extends StatefulWidget {
57 /// Creates a [RestorationScope].
58 ///
59 /// Providing null as the [restorationId] turns off state restoration for
60 /// the [child] and its descendants.
61 const RestorationScope({super.key, required this.restorationId, required this.child});
62
63 /// Returns the [RestorationBucket] inserted into the widget tree by the
64 /// closest ancestor [RestorationScope] of `context`.
65 ///
66 /// {@template flutter.widgets.restoration.RestorationScope.bucket_warning}
67 /// To avoid accidentally overwriting data already stored in the bucket by its
68 /// owner, data should not be stored directly in the bucket returned by this
69 /// method. Instead, consider claiming a child bucket from the returned bucket
70 /// (via [RestorationBucket.claimChild]) and store the restoration data in
71 /// that child.
72 /// {@endtemplate}
73 ///
74 /// This method returns null if state restoration is turned off for this
75 /// subtree.
76 ///
77 /// Calling this method will create a dependency on the closest
78 /// [RestorationScope] in the [context], if there is one.
79 ///
80 /// See also:
81 ///
82 /// * [RestorationScope.maybeOf], which is similar to this method, but asserts
83 /// if no [RestorationScope] ancestor is found.
84 static RestorationBucket? maybeOf(BuildContext context) {
85 return context.dependOnInheritedWidgetOfExactType<UnmanagedRestorationScope>()?.bucket;
86 }
87
88 /// Returns the [RestorationBucket] inserted into the widget tree by the
89 /// closest ancestor [RestorationScope] of `context`.
90 ///
91 /// {@macro flutter.widgets.restoration.RestorationScope.bucket_warning}
92 ///
93 /// This method will assert in debug mode and throw an exception in release
94 /// mode if state restoration is turned off for this subtree.
95 ///
96 /// Calling this method will create a dependency on the closest
97 /// [RestorationScope] in the [context].
98 ///
99 /// See also:
100 ///
101 /// * [RestorationScope.maybeOf], which is similar to this method, but returns
102 /// null if no [RestorationScope] ancestor is found.
103 static RestorationBucket of(BuildContext context) {
104 final RestorationBucket? bucket = maybeOf(context);
105 assert(() {
106 if (bucket == null) {
107 throw FlutterError.fromParts(<DiagnosticsNode>[
108 ErrorSummary(
109 'RestorationScope.of() was called with a context that does not '
110 'contain a RestorationScope widget. ',
111 ),
112 ErrorDescription(
113 'No RestorationScope widget ancestor could be found starting from '
114 'the context that was passed to RestorationScope.of(). This can '
115 'happen because you are using a widget that looks for a '
116 'RestorationScope ancestor, but no such ancestor exists.\n'
117 'The context used was:\n'
118 ' $context',
119 ),
120 ErrorHint(
121 'State restoration must be enabled for a RestorationScope to exist. '
122 'This can be done by passing a restorationScopeId to MaterialApp, '
123 'CupertinoApp, or WidgetsApp at the root of the widget tree or by '
124 'wrapping the widget tree in a RootRestorationScope.',
125 ),
126 ]);
127 }
128 return true;
129 }());
130 return bucket!;
131 }
132
133 /// The widget below this widget in the tree.
134 ///
135 /// {@macro flutter.widgets.ProxyWidget.child}
136 final Widget child;
137
138 /// The restoration ID used by this widget to obtain a child bucket from the
139 /// surrounding [RestorationScope].
140 ///
141 /// The child bucket obtained from the surrounding scope is made available to
142 /// descendant widgets via [RestorationScope.of].
143 ///
144 /// If this is null, [RestorationScope.of] invoked by descendants will return
145 /// null which effectively turns off state restoration for this subtree.
146 final String? restorationId;
147
148 @override
149 State<RestorationScope> createState() => _RestorationScopeState();
150}
151
152class _RestorationScopeState extends State<RestorationScope> with RestorationMixin {
153 @override
154 String? get restorationId => widget.restorationId;
155
156 @override
157 void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
158 // Nothing to do.
159 // The bucket gets injected into the widget tree in the build method.
160 }
161
162 @override
163 Widget build(BuildContext context) {
164 return UnmanagedRestorationScope(
165 bucket: bucket, // `bucket` is provided by the RestorationMixin.
166 child: widget.child,
167 );
168 }
169}
170
171/// Inserts a provided [RestorationBucket] into the widget tree and makes it
172/// available to descendants via [RestorationScope.of].
173///
174/// {@macro flutter.widgets.RestorationScope}
175///
176/// If [bucket] is null, no restoration bucket is made available to descendant
177/// widgets ([RestorationScope.of] invoked from a descendant will return null).
178/// This effectively turns off state restoration for the subtree because no
179/// bucket for storing restoration data is made available.
180///
181/// See also:
182///
183/// * [RestorationScope], which inserts a bucket obtained from a surrounding
184/// restoration scope into the widget tree and makes it accessible
185/// for descendants via [RestorationScope.of].
186/// * [RootRestorationScope], which inserts the root bucket provided by
187/// the [RestorationManager] into the widget tree and makes it accessible
188/// for descendants via [RestorationScope.of].
189/// * [RestorationMixin], which may be used in [State] objects to manage the
190/// restoration data of a [StatefulWidget] instead of manually interacting
191/// with [RestorationScope]s and [RestorationBucket]s.
192/// * [RestorationManager], which describes the basic concepts of state
193/// restoration in Flutter.
194class UnmanagedRestorationScope extends InheritedWidget {
195 /// Creates an [UnmanagedRestorationScope].
196 ///
197 /// When [bucket] is null state restoration is turned off for the [child] and
198 /// its descendants.
199 const UnmanagedRestorationScope({super.key, this.bucket, required super.child});
200
201 /// The [RestorationBucket] that this widget will insert into the widget tree.
202 ///
203 /// Descendant widgets may obtain this bucket via [RestorationScope.of].
204 final RestorationBucket? bucket;
205
206 @override
207 bool updateShouldNotify(UnmanagedRestorationScope oldWidget) {
208 return oldWidget.bucket != bucket;
209 }
210}
211
212/// Inserts a child bucket of [RestorationManager.rootBucket] into the widget
213/// tree and makes it available to descendants via [RestorationScope.of].
214///
215/// This widget is usually used near the root of the widget tree to enable the
216/// state restoration functionality for the application. For all other use
217/// cases, consider using a regular [RestorationScope] instead.
218///
219/// The root restoration bucket can only be retrieved asynchronously from the
220/// [RestorationManager]. To ensure that the provided [child] has its
221/// restoration data available the first time it builds, the
222/// [RootRestorationScope] will build an empty [Container] instead of the actual
223/// [child] until the root bucket is available. To hide the empty container from
224/// the eyes of users, the [RootRestorationScope] also delays rendering the
225/// first frame while the container is shown. On platforms that show a splash
226/// screen on app launch the splash screen is kept up (hiding the empty
227/// container) until the bucket is available and the [child] is ready to be
228/// build.
229///
230/// The exact behavior of this widget depends on its ancestors: When the
231/// [RootRestorationScope] does not find an ancestor restoration bucket via
232/// [RestorationScope.of] it will claim a child bucket from the root restoration
233/// bucket ([RestorationManager.rootBucket]) using the provided [restorationId]
234/// and inserts that bucket into the widget tree where descendants may access it
235/// via [RestorationScope.of]. If the [RootRestorationScope] finds a non-null
236/// ancestor restoration bucket via [RestorationScope.of] it will behave like a
237/// regular [RestorationScope] instead: It will claim a child bucket from that
238/// ancestor and insert that child into the widget tree.
239///
240/// Unlike the [RestorationScope] widget, the [RootRestorationScope] will
241/// guarantee that descendants have a bucket available for storing restoration
242/// data as long as [restorationId] is not null and [RestorationManager] is
243/// able to provide a root bucket. In other words, it will force-enable
244/// state restoration for the subtree if [restorationId] is not null.
245///
246/// If [restorationId] is null, no bucket is made available to descendants,
247/// which effectively turns off state restoration for this subtree.
248///
249/// See also:
250///
251/// * [RestorationScope], which inserts a bucket obtained from a surrounding
252/// restoration scope into the widget tree and makes it accessible
253/// for descendants via [RestorationScope.of].
254/// * [UnmanagedRestorationScope], which inserts a provided [RestorationBucket]
255/// into the widget tree and makes it accessible for descendants via
256/// [RestorationScope.of].
257/// * [RestorationMixin], which may be used in [State] objects to manage the
258/// restoration data of a [StatefulWidget] instead of manually interacting
259/// with [RestorationScope]s and [RestorationBucket]s.
260/// * [RestorationManager], which describes the basic concepts of state
261/// restoration in Flutter.
262class RootRestorationScope extends StatefulWidget {
263 /// Creates a [RootRestorationScope].
264 ///
265 /// Providing null as the [restorationId] turns off state restoration for
266 /// the [child] and its descendants.
267 const RootRestorationScope({super.key, required this.restorationId, required this.child});
268
269 /// The widget below this widget in the tree.
270 ///
271 /// {@macro flutter.widgets.ProxyWidget.child}
272 final Widget child;
273
274 /// The restoration ID used to identify the child bucket that this widget
275 /// will insert into the tree.
276 ///
277 /// If this is null, no bucket is made available to descendants and state
278 /// restoration for the subtree is essentially turned off.
279 final String? restorationId;
280
281 @override
282 State<RootRestorationScope> createState() => _RootRestorationScopeState();
283}
284
285class _RootRestorationScopeState extends State<RootRestorationScope> {
286 bool? _okToRenderBlankContainer;
287 bool _rootBucketValid = false;
288 RestorationBucket? _rootBucket;
289 RestorationBucket? _ancestorBucket;
290
291 @override
292 void didChangeDependencies() {
293 super.didChangeDependencies();
294 _ancestorBucket = RestorationScope.maybeOf(context);
295 _loadRootBucketIfNecessary();
296 _okToRenderBlankContainer ??= widget.restorationId != null && _needsRootBucketInserted;
297 }
298
299 @override
300 void didUpdateWidget(RootRestorationScope oldWidget) {
301 super.didUpdateWidget(oldWidget);
302 _loadRootBucketIfNecessary();
303 }
304
305 bool get _needsRootBucketInserted => _ancestorBucket == null;
306
307 bool get _isWaitingForRootBucket {
308 return widget.restorationId != null && _needsRootBucketInserted && !_rootBucketValid;
309 }
310
311 bool _isLoadingRootBucket = false;
312
313 void _loadRootBucketIfNecessary() {
314 if (_isWaitingForRootBucket && !_isLoadingRootBucket) {
315 _isLoadingRootBucket = true;
316 RendererBinding.instance.deferFirstFrame();
317 ServicesBinding.instance.restorationManager.rootBucket.then((RestorationBucket? bucket) {
318 _isLoadingRootBucket = false;
319 if (mounted) {
320 ServicesBinding.instance.restorationManager.addListener(_replaceRootBucket);
321 setState(() {
322 _rootBucket = bucket;
323 _rootBucketValid = true;
324 _okToRenderBlankContainer = false;
325 });
326 }
327 RendererBinding.instance.allowFirstFrame();
328 });
329 }
330 }
331
332 void _replaceRootBucket() {
333 _rootBucketValid = false;
334 _rootBucket = null;
335 ServicesBinding.instance.restorationManager.removeListener(_replaceRootBucket);
336 _loadRootBucketIfNecessary();
337 assert(!_isWaitingForRootBucket); // Ensure that load finished synchronously.
338 }
339
340 @override
341 void dispose() {
342 if (_rootBucketValid) {
343 ServicesBinding.instance.restorationManager.removeListener(_replaceRootBucket);
344 }
345 super.dispose();
346 }
347
348 @override
349 Widget build(BuildContext context) {
350 if (_okToRenderBlankContainer! && _isWaitingForRootBucket) {
351 return const SizedBox.shrink();
352 }
353
354 return UnmanagedRestorationScope(
355 bucket: _ancestorBucket ?? _rootBucket,
356 child: RestorationScope(restorationId: widget.restorationId, child: widget.child),
357 );
358 }
359}
360
361/// Manages an object of type `T`, whose value a [State] object wants to have
362/// restored during state restoration.
363///
364/// The property wraps an object of type `T`. It knows how to store its value in
365/// the restoration data and it knows how to re-instantiate that object from the
366/// information it previously stored in the restoration data.
367///
368/// The knowledge of how to store the wrapped object in the restoration data is
369/// encoded in the [toPrimitives] method and the knowledge of how to
370/// re-instantiate the object from that data is encoded in the [fromPrimitives]
371/// method. A call to [toPrimitives] must return a representation of the wrapped
372/// object that can be serialized with the [StandardMessageCodec]. If any
373/// collections (e.g. [List]s, [Map]s, etc.) are returned, they must not be
374/// modified after they have been returned from [toPrimitives]. At a later point
375/// in time (which may be after the application restarted), the data obtained
376/// from [toPrimitives] may be handed back to the property's [fromPrimitives]
377/// method to restore it to the previous state described by that data.
378///
379/// A [RestorableProperty] needs to be registered to a [RestorationMixin] using
380/// a restoration ID that is unique within the mixin. The [RestorationMixin]
381/// provides and manages the [RestorationBucket], in which the data returned by
382/// [toPrimitives] is stored.
383///
384/// Whenever the value returned by [toPrimitives] (or the [enabled] getter)
385/// changes, the [RestorableProperty] must call [notifyListeners]. This will
386/// trigger the [RestorationMixin] to update the data it has stored for the
387/// property in its [RestorationBucket] to the latest information returned by
388/// [toPrimitives].
389///
390/// When the property is registered with the [RestorationMixin], the mixin
391/// checks whether there is any restoration data available for the property. If
392/// data is available, the mixin calls [fromPrimitives] on the property, which
393/// must return an object that matches the object the property wrapped when the
394/// provided restoration data was obtained from [toPrimitives]. If no
395/// restoration data is available to restore the property's wrapped object from,
396/// the mixin calls [createDefaultValue]. The value returned by either of those
397/// methods is then handed to the property's [initWithValue] method.
398///
399/// Usually, subclasses of [RestorableProperty] hold on to the value provided to
400/// them in [initWithValue] and make it accessible to the [State] object that
401/// owns the property. This [RestorableProperty] base class, however, has no
402/// opinion about what to do with the value provided to [initWithValue].
403///
404/// The [RestorationMixin] may call [fromPrimitives]/[createDefaultValue]
405/// followed by [initWithValue] multiple times throughout the life of a
406/// [RestorableProperty]: Whenever new restoration data is made available to the
407/// [RestorationMixin] the property is registered with, the cycle of calling
408/// [fromPrimitives] (if the new restoration data contains information to
409/// restore the property from) or [createDefaultValue] (if no information for
410/// the property is available in the new restoration data) followed by a call to
411/// [initWithValue] repeats. Whenever [initWithValue] is called, the property
412/// should forget the old value it was wrapping and re-initialize itself with
413/// the newly provided value.
414///
415/// In a typical use case, a subclass of [RestorableProperty] is instantiated
416/// either to initialize a member variable of a [State] object or within
417/// [State.initState]. It is then registered to a [RestorationMixin] in
418/// [RestorationMixin.restoreState] and later [dispose]ed in [State.dispose].
419/// For less common use cases (e.g. if the value stored in a
420/// [RestorableProperty] is only needed while the [State] object is in a certain
421/// state), a [RestorableProperty] may be registered with a [RestorationMixin]
422/// any time after [RestorationMixin.restoreState] has been called for the first
423/// time. A [RestorableProperty] may also be unregistered from a
424/// [RestorationMixin] before the owning [State] object is disposed by calling
425/// [RestorationMixin.unregisterFromRestoration]. This is uncommon, though, and
426/// will delete the information that the property contributed from the
427/// restoration data (meaning the value of the property will no longer be
428/// restored during a future state restoration).
429///
430/// See also:
431///
432/// * [RestorableValue], which is a [RestorableProperty] that makes the wrapped
433/// value accessible to the owning [State] object via a `value`
434/// getter and setter.
435/// * [RestorationMixin], to which a [RestorableProperty] must be registered.
436/// * [RestorationManager], which describes how state restoration works in
437/// Flutter.
438abstract class RestorableProperty<T> extends ChangeNotifier {
439 /// Creates a [RestorableProperty].
440 RestorableProperty() {
441 if (kFlutterMemoryAllocationsEnabled) {
442 ChangeNotifier.maybeDispatchObjectCreation(this);
443 }
444 }
445
446 /// Called by the [RestorationMixin] if no restoration data is available to
447 /// restore the value of the property from to obtain the default value for the
448 /// property.
449 ///
450 /// The method returns the default value that the property should wrap if no
451 /// restoration data is available. After this is called, [initWithValue] will
452 /// be called with this method's return value.
453 ///
454 /// The method may be called multiple times throughout the life of the
455 /// [RestorableProperty]. Whenever new restoration data has been provided to
456 /// the [RestorationMixin] the property is registered to, either this method
457 /// or [fromPrimitives] is called before [initWithValue] is invoked.
458 T createDefaultValue();
459
460 /// Called by the [RestorationMixin] to convert the `data` previously
461 /// retrieved from [toPrimitives] back into an object of type `T` that this
462 /// property should wrap.
463 ///
464 /// The object returned by this method is passed to [initWithValue] to restore
465 /// the value that this property is wrapping to the value described by the
466 /// provided `data`.
467 ///
468 /// The method may be called multiple times throughout the life of the
469 /// [RestorableProperty]. Whenever new restoration data has been provided to
470 /// the [RestorationMixin] the property is registered to, either this method
471 /// or [createDefaultValue] is called before [initWithValue] is invoked.
472 T fromPrimitives(Object? data);
473
474 /// Called by the [RestorationMixin] with the `value` returned by either
475 /// [createDefaultValue] or [fromPrimitives] to set the value that this
476 /// property currently wraps.
477 ///
478 /// The [initWithValue] method may be called multiple times throughout the
479 /// life of the [RestorableProperty] whenever new restoration data has been
480 /// provided to the [RestorationMixin] the property is registered to. When
481 /// [initWithValue] is called, the property should forget its previous value
482 /// and re-initialize itself to the newly provided `value`.
483 void initWithValue(T value);
484
485 /// Called by the [RestorationMixin] to retrieve the information that this
486 /// property wants to store in the restoration data.
487 ///
488 /// The returned object must be serializable with the [StandardMessageCodec]
489 /// and if it includes any collections, those should not be modified after
490 /// they have been returned by this method.
491 ///
492 /// The information returned by this method may be handed back to the property
493 /// in a call to [fromPrimitives] at a later point in time (possibly after the
494 /// application restarted) to restore the value that the property is currently
495 /// wrapping.
496 ///
497 /// When the value returned by this method changes, the property must call
498 /// [notifyListeners]. The [RestorationMixin] will invoke this method whenever
499 /// the property's listeners are notified.
500 Object? toPrimitives();
501
502 /// Whether the object currently returned by [toPrimitives] should be included
503 /// in the restoration state.
504 ///
505 /// When this returns false, no information is included in the restoration
506 /// data for this property and the property will be initialized to its default
507 /// value (obtained from [createDefaultValue]) the next time that restoration
508 /// data is used for state restoration.
509 ///
510 /// Whenever the value returned by this getter changes, [notifyListeners] must
511 /// be called. When the value changes from true to false, the information last
512 /// retrieved from [toPrimitives] is removed from the restoration data. When
513 /// it changes from false to true, [toPrimitives] is invoked to add the latest
514 /// restoration information provided by this property to the restoration data.
515 bool get enabled => true;
516
517 bool _disposed = false;
518
519 @override
520 void dispose() {
521 assert(
522 ChangeNotifier.debugAssertNotDisposed(this),
523 ); // FYI, This uses ChangeNotifier's _debugDisposed, not _disposed.
524 _owner?._unregister(this);
525 super.dispose();
526 _disposed = true;
527 }
528
529 // ID under which the property has been registered with the RestorationMixin.
530 String? _restorationId;
531 RestorationMixin? _owner;
532 void _register(String restorationId, RestorationMixin owner) {
533 assert(ChangeNotifier.debugAssertNotDisposed(this));
534 _restorationId = restorationId;
535 _owner = owner;
536 }
537
538 void _unregister() {
539 assert(ChangeNotifier.debugAssertNotDisposed(this));
540 assert(_restorationId != null);
541 assert(_owner != null);
542 _restorationId = null;
543 _owner = null;
544 }
545
546 /// The [State] object that this property is registered with.
547 ///
548 /// Must only be called when [isRegistered] is true.
549 @protected
550 State get state {
551 assert(isRegistered);
552 assert(ChangeNotifier.debugAssertNotDisposed(this));
553 return _owner!;
554 }
555
556 /// Whether this property is currently registered with a [RestorationMixin].
557 @protected
558 bool get isRegistered {
559 assert(ChangeNotifier.debugAssertNotDisposed(this));
560 return _restorationId != null;
561 }
562}
563
564/// Manages the restoration data for a [State] object of a [StatefulWidget].
565///
566/// Restoration data can be serialized out and, at a later point in time, be
567/// used to restore the stateful members in the [State] object to the same
568/// values they had when the data was generated.
569///
570/// This mixin organizes the restoration data of a [State] object in
571/// [RestorableProperty]. All the information that the [State] object wants to
572/// get restored during state restoration need to be saved in a subclass of
573/// [RestorableProperty]. For example, to restore the count value in a counter
574/// app, that value should be stored in a member variable of type
575/// [RestorableInt] instead of a plain member variable of type [int].
576///
577/// The mixin ensures that the current values of the [RestorableProperty]s are
578/// serialized as part of the restoration state. It is up to the [State] to
579/// ensure that the data stored in the properties is always up to date. When the
580/// widget is restored from previously generated restoration data, the values of
581/// the [RestorableProperty]s are automatically restored to the values that had
582/// when the restoration data was serialized out.
583///
584/// Within a [State] that uses this mixin, [RestorableProperty]s are usually
585/// instantiated to initialize member variables. Users of the mixin must
586/// override [restoreState] and register their previously instantiated
587/// [RestorableProperty]s in this method by calling [registerForRestoration].
588/// The mixin calls this method for the first time right after
589/// [State.initState]. After registration, the values stored in the property
590/// have either been restored to their previous value or - if no restoration
591/// data for restoring is available - they are initialized with a
592/// property-specific default value. At the end of a [State] object's life
593/// cycle, all restorable properties must be disposed in [State.dispose].
594///
595/// In addition to being invoked right after [State.initState], [restoreState]
596/// is invoked again when new restoration data has been provided to the mixin.
597/// When this happens, the [State] object must re-register all properties with
598/// [registerForRestoration] again to restore them to their previous values as
599/// described by the new restoration data. All initialization logic that depends
600/// on the current value of a restorable property should be included in the
601/// [restoreState] method to ensure it re-executes when the properties are
602/// restored to a different value during the life time of the [State] object.
603///
604/// Internally, the mixin stores the restoration data from all registered
605/// properties in a [RestorationBucket] claimed from the surrounding
606/// [RestorationScope] using the [State]-provided [restorationId]. The
607/// [restorationId] must be unique in the surrounding [RestorationScope]. State
608/// restoration is disabled for the [State] object using this mixin if
609/// [restorationId] is null or when there is no surrounding [RestorationScope].
610/// In that case, the values of the registered properties will not be restored
611/// during state restoration.
612///
613/// The [RestorationBucket] used to store the registered properties is available
614/// via the [bucket] getter. Interacting directly with the bucket is uncommon,
615/// but the [State] object may make this bucket available for its descendants to
616/// claim child buckets from. For that, the [bucket] is injected into the widget
617/// tree in [State.build] with the help of an [UnmanagedRestorationScope].
618///
619/// The [bucket] getter returns null if state restoration is turned off. If
620/// state restoration is turned on or off during the lifetime of the widget
621/// (e.g. because [restorationId] changes from null to non-null) the value
622/// returned by the getter will also change from null to non-null or vice versa.
623/// The mixin calls [didToggleBucket] on itself to notify the [State] object
624/// about this change. Overriding this method is not necessary as long as the
625/// [State] object does not directly interact with the [bucket].
626///
627/// Whenever the value returned by [restorationId] changes,
628/// [didUpdateRestorationId] must be called (unless the change already triggers
629/// a call to [didUpdateWidget]).
630///
631/// {@tool dartpad}
632/// This example demonstrates how to make a simple counter app restorable by
633/// using the [RestorationMixin] and a [RestorableInt].
634///
635/// ** See code in examples/api/lib/widgets/restoration/restoration_mixin.0.dart **
636/// {@end-tool}
637///
638/// See also:
639///
640/// * [RestorableProperty], which is the base class for all restoration
641/// properties managed by this mixin.
642/// * [RestorationManager], which describes how state restoration in Flutter
643/// works.
644/// * [RestorationScope], which creates a new namespace for restoration IDs
645/// in the widget tree.
646@optionalTypeArgs
647mixin RestorationMixin<S extends StatefulWidget> on State<S> {
648 /// The restoration ID used for the [RestorationBucket] in which the mixin
649 /// will store the restoration data of all registered properties.
650 ///
651 /// The restoration ID is used to claim a child [RestorationScope] from the
652 /// surrounding [RestorationScope] (accessed via [RestorationScope.of]) and
653 /// the ID must be unique in that scope (otherwise an exception is triggered
654 /// in debug mode).
655 ///
656 /// State restoration for this mixin is turned off when this getter returns
657 /// null or when there is no surrounding [RestorationScope] available. When
658 /// state restoration is turned off, the values of the registered properties
659 /// cannot be restored.
660 ///
661 /// Whenever the value returned by this getter changes,
662 /// [didUpdateRestorationId] must be called unless the (unless the change
663 /// already triggered a call to [didUpdateWidget]).
664 ///
665 /// The restoration ID returned by this getter is often provided in the
666 /// constructor of the [StatefulWidget] that this [State] object is associated
667 /// with.
668 @protected
669 String? get restorationId;
670
671 /// The [RestorationBucket] used for the restoration data of the
672 /// [RestorableProperty]s registered to this mixin.
673 ///
674 /// The bucket has been claimed from the surrounding [RestorationScope] using
675 /// [restorationId].
676 ///
677 /// The getter returns null if state restoration is turned off. When state
678 /// restoration is turned on or off during the lifetime of this mixin (and
679 /// hence the return value of this getter switches between null and non-null)
680 /// [didToggleBucket] is called.
681 ///
682 /// Interacting directly with this bucket is uncommon. However, the bucket may
683 /// be injected into the widget tree in the [State]'s `build` method using an
684 /// [UnmanagedRestorationScope]. That allows descendants to claim child
685 /// buckets from this bucket for their own restoration needs.
686 RestorationBucket? get bucket => _bucket;
687 RestorationBucket? _bucket;
688
689 /// Called to initialize or restore the [RestorableProperty]s used by the
690 /// [State] object.
691 ///
692 /// This method is always invoked at least once right after [State.initState]
693 /// to register the [RestorableProperty]s with the mixin even when state
694 /// restoration is turned off or no restoration data is available for this
695 /// [State] object.
696 ///
697 /// Typically, [registerForRestoration] is called from this method to register
698 /// all [RestorableProperty]s used by the [State] object with the mixin. The
699 /// registration will either restore the property's value to the value
700 /// described by the restoration data, if available, or, if no restoration
701 /// data is available - initialize it to a property-specific default value.
702 ///
703 /// The method is called again whenever new restoration data (in the form of a
704 /// new [bucket]) has been provided to the mixin. When that happens, the
705 /// [State] object must re-register all previously registered properties,
706 /// which will restore their values to the value described by the new
707 /// restoration data.
708 ///
709 /// Since the method may change the value of the registered properties when
710 /// new restoration state is provided, all initialization logic that depends
711 /// on a specific value of a [RestorableProperty] should be included in this
712 /// method. That way, that logic re-executes when the [RestorableProperty]s
713 /// have their values restored from newly provided restoration data.
714 ///
715 /// The first time the method is invoked, the provided `oldBucket` argument is
716 /// always null. In subsequent calls triggered by new restoration data in the
717 /// form of a new bucket, the argument given is the previous value of
718 /// [bucket].
719 @mustCallSuper
720 @protected
721 void restoreState(RestorationBucket? oldBucket, bool initialRestore);
722
723 /// Called when [bucket] switches between null and non-null values.
724 ///
725 /// [State] objects that wish to directly interact with the bucket may
726 /// override this method to store additional values in the bucket when one
727 /// becomes available or to save values stored in a bucket elsewhere when the
728 /// bucket goes away. This is uncommon and storing those values in
729 /// [RestorableProperty]s should be considered instead.
730 ///
731 /// The `oldBucket` is provided to the method when the [bucket] getter changes
732 /// from non-null to null. The `oldBucket` argument is null when the [bucket]
733 /// changes from null to non-null.
734 ///
735 /// See also:
736 ///
737 /// * [restoreState], which is called when the [bucket] changes from one
738 /// non-null value to another non-null value.
739 @mustCallSuper
740 @protected
741 void didToggleBucket(RestorationBucket? oldBucket) {
742 // When a bucket is replaced, must `restoreState` is called instead.
743 assert(_bucket?.isReplacing != true);
744 }
745
746 // Maps properties to their listeners.
747 final Map<RestorableProperty<Object?>, VoidCallback> _properties =
748 <RestorableProperty<Object?>, VoidCallback>{};
749
750 /// Registers a [RestorableProperty] for state restoration.
751 ///
752 /// The registration associates the provided `property` with the provided
753 /// `restorationId`. If restoration data is available for the provided
754 /// `restorationId`, the property's value is restored to the value described
755 /// by the restoration data. If no restoration data is available, the property
756 /// will be initialized to a property-specific default value.
757 ///
758 /// Each property within a [State] object must be registered under a unique
759 /// ID. Only registered properties will have their values restored during
760 /// state restoration.
761 ///
762 /// Typically, this method is called from within [restoreState] to register
763 /// all restorable properties of the owning [State] object. However, if a
764 /// given [RestorableProperty] is only needed when certain conditions are met
765 /// within the [State], [registerForRestoration] may also be called at any
766 /// time after [restoreState] has been invoked for the first time.
767 ///
768 /// A property that has been registered outside of [restoreState] must be
769 /// re-registered within [restoreState] the next time that method is called
770 /// unless it has been unregistered with [unregisterFromRestoration].
771 @protected
772 void registerForRestoration(RestorableProperty<Object?> property, String restorationId) {
773 assert(
774 property._restorationId == null ||
775 (_debugDoingRestore && property._restorationId == restorationId),
776 'Property is already registered under ${property._restorationId}.',
777 );
778 assert(
779 _debugDoingRestore ||
780 !_properties.keys
781 .map((RestorableProperty<Object?> r) => r._restorationId)
782 .contains(restorationId),
783 '"$restorationId" is already registered to another property.',
784 );
785 final bool hasSerializedValue = bucket?.contains(restorationId) ?? false;
786 final Object? initialValue = hasSerializedValue
787 ? property.fromPrimitives(bucket!.read<Object>(restorationId))
788 : property.createDefaultValue();
789
790 if (!property.isRegistered) {
791 property._register(restorationId, this);
792 void listener() {
793 if (bucket == null) {
794 return;
795 }
796 _updateProperty(property);
797 }
798
799 property.addListener(listener);
800 _properties[property] = listener;
801 }
802
803 assert(
804 property._restorationId == restorationId &&
805 property._owner == this &&
806 _properties.containsKey(property),
807 );
808
809 property.initWithValue(initialValue);
810 if (!hasSerializedValue && property.enabled && bucket != null) {
811 _updateProperty(property);
812 }
813
814 assert(() {
815 _debugPropertiesWaitingForReregistration?.remove(property);
816 return true;
817 }());
818 }
819
820 /// Unregisters a [RestorableProperty] from state restoration.
821 ///
822 /// The value of the `property` is removed from the restoration data and it
823 /// will not be restored if that data is used in a future state restoration.
824 ///
825 /// Calling this method is uncommon, but may be necessary if the data of a
826 /// [RestorableProperty] is only relevant when the [State] object is in a
827 /// certain state. When the data of a property is no longer necessary to
828 /// restore the internal state of a [State] object, it may be removed from the
829 /// restoration data by calling this method.
830 @protected
831 void unregisterFromRestoration(RestorableProperty<Object?> property) {
832 assert(property._owner == this);
833 _bucket?.remove<Object?>(property._restorationId!);
834 _unregister(property);
835 }
836
837 /// Must be called when the value returned by [restorationId] changes.
838 ///
839 /// This method is automatically called from [didUpdateWidget]. Therefore,
840 /// manually invoking this method may be omitted when the change in
841 /// [restorationId] was caused by an updated widget.
842 @protected
843 void didUpdateRestorationId() {
844 // There's nothing to do if:
845 // - We don't have a parent to claim a bucket from.
846 // - Our current bucket already uses the provided restoration ID.
847 // - There's a restore pending, which means that didChangeDependencies
848 // will be called and we handle the rename there.
849 if (_currentParent == null || _bucket?.restorationId == restorationId || restorePending) {
850 return;
851 }
852
853 final RestorationBucket? oldBucket = _bucket;
854 assert(!restorePending);
855 final bool didReplaceBucket = _updateBucketIfNecessary(
856 parent: _currentParent,
857 restorePending: false,
858 );
859 if (didReplaceBucket) {
860 assert(oldBucket != _bucket);
861 assert(_bucket == null || oldBucket == null);
862 oldBucket?.dispose();
863 }
864 }
865
866 @override
867 void didUpdateWidget(S oldWidget) {
868 super.didUpdateWidget(oldWidget);
869 didUpdateRestorationId();
870 }
871
872 /// Whether [restoreState] will be called at the beginning of the next build
873 /// phase.
874 ///
875 /// Returns true when new restoration data has been provided to the mixin, but
876 /// the registered [RestorableProperty]s have not been restored to their new
877 /// values (as described by the new restoration data) yet. The properties will
878 /// get the values restored when [restoreState] is invoked at the beginning of
879 /// the next build cycle.
880 ///
881 /// While this is true, [bucket] will also still return the old bucket with
882 /// the old restoration data. It will update to the new bucket with the new
883 /// data just before [restoreState] is invoked.
884 bool get restorePending {
885 if (_firstRestorePending) {
886 return true;
887 }
888 if (restorationId == null) {
889 return false;
890 }
891 final RestorationBucket? potentialNewParent = RestorationScope.maybeOf(context);
892 return potentialNewParent != _currentParent && (potentialNewParent?.isReplacing ?? false);
893 }
894
895 List<RestorableProperty<Object?>>? _debugPropertiesWaitingForReregistration;
896 bool get _debugDoingRestore => _debugPropertiesWaitingForReregistration != null;
897
898 bool _firstRestorePending = true;
899 RestorationBucket? _currentParent;
900
901 @override
902 void didChangeDependencies() {
903 super.didChangeDependencies();
904
905 final RestorationBucket? oldBucket = _bucket;
906 final bool needsRestore = restorePending;
907 _currentParent = RestorationScope.maybeOf(context);
908
909 final bool didReplaceBucket = _updateBucketIfNecessary(
910 parent: _currentParent,
911 restorePending: needsRestore,
912 );
913
914 if (needsRestore) {
915 _doRestore(oldBucket);
916 }
917 if (didReplaceBucket) {
918 assert(oldBucket != _bucket);
919 oldBucket?.dispose();
920 }
921 }
922
923 void _doRestore(RestorationBucket? oldBucket) {
924 assert(() {
925 _debugPropertiesWaitingForReregistration = _properties.keys.toList();
926 return true;
927 }());
928
929 restoreState(oldBucket, _firstRestorePending);
930 _firstRestorePending = false;
931
932 assert(() {
933 if (_debugPropertiesWaitingForReregistration!.isNotEmpty) {
934 throw FlutterError.fromParts(<DiagnosticsNode>[
935 ErrorSummary(
936 'Previously registered RestorableProperties must be re-registered in "restoreState".',
937 ),
938 ErrorDescription(
939 'The RestorableProperties with the following IDs were not re-registered to $this when '
940 '"restoreState" was called:',
941 ),
942 ..._debugPropertiesWaitingForReregistration!.map(
943 (RestorableProperty<Object?> property) =>
944 ErrorDescription(' * ${property._restorationId}'),
945 ),
946 ]);
947 }
948 _debugPropertiesWaitingForReregistration = null;
949 return true;
950 }());
951 }
952
953 // Returns true if `bucket` has been replaced with a new bucket. It's the
954 // responsibility of the caller to dispose the old bucket when this returns true.
955 bool _updateBucketIfNecessary({
956 required RestorationBucket? parent,
957 required bool restorePending,
958 }) {
959 if (restorationId == null || parent == null) {
960 final bool didReplace = _setNewBucketIfNecessary(
961 newBucket: null,
962 restorePending: restorePending,
963 );
964 assert(_bucket == null);
965 return didReplace;
966 }
967 assert(restorationId != null);
968 if (restorePending || _bucket == null) {
969 final RestorationBucket newBucket = parent.claimChild(restorationId!, debugOwner: this);
970 final bool didReplace = _setNewBucketIfNecessary(
971 newBucket: newBucket,
972 restorePending: restorePending,
973 );
974 assert(_bucket == newBucket);
975 return didReplace;
976 }
977 // We have an existing bucket, make sure it has the right parent and id.
978 assert(_bucket != null);
979 assert(!restorePending);
980 _bucket!.rename(restorationId!);
981 parent.adoptChild(_bucket!);
982 return false;
983 }
984
985 // Returns true if `bucket` has been replaced with a new bucket. It's the
986 // responsibility of the caller to dispose the old bucket when this returns true.
987 bool _setNewBucketIfNecessary({
988 required RestorationBucket? newBucket,
989 required bool restorePending,
990 }) {
991 if (newBucket == _bucket) {
992 return false;
993 }
994 final RestorationBucket? oldBucket = _bucket;
995 _bucket = newBucket;
996 if (!restorePending) {
997 // Write the current property values into the new bucket to persist them.
998 if (_bucket != null) {
999 _properties.keys.forEach(_updateProperty);
1000 }
1001 didToggleBucket(oldBucket);
1002 }
1003 return true;
1004 }
1005
1006 void _updateProperty(RestorableProperty<Object?> property) {
1007 if (property.enabled) {
1008 _bucket?.write(property._restorationId!, property.toPrimitives());
1009 } else {
1010 _bucket?.remove<Object>(property._restorationId!);
1011 }
1012 }
1013
1014 void _unregister(RestorableProperty<Object?> property) {
1015 final VoidCallback listener = _properties.remove(property)!;
1016 assert(() {
1017 _debugPropertiesWaitingForReregistration?.remove(property);
1018 return true;
1019 }());
1020 property.removeListener(listener);
1021 property._unregister();
1022 }
1023
1024 @override
1025 void dispose() {
1026 _properties.forEach((RestorableProperty<Object?> property, VoidCallback listener) {
1027 if (!property._disposed) {
1028 property.removeListener(listener);
1029 }
1030 });
1031 _bucket?.dispose();
1032 _bucket = null;
1033 super.dispose();
1034 }
1035}
1036