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