-
Notifications
You must be signed in to change notification settings - Fork 30.3k
Expand file tree
/
Copy pathrestoration.dart
More file actions
1018 lines (947 loc) · 41.7 KB
/
restoration.dart
File metadata and controls
1018 lines (947 loc) · 41.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// @docImport 'package:flutter/widgets.dart';
///
/// @docImport 'binding.dart';
library;
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'message_codecs.dart';
import 'system_channels.dart';
export 'dart:typed_data' show Uint8List;
typedef _BucketVisitor = void Function(RestorationBucket bucket);
/// Manages the restoration data in the framework and synchronizes it with the
/// engine.
///
/// Restoration data can be serialized out and - at a later point in time - be
/// used to restore the application to the previous state described by the
/// serialized data. Mobile operating systems use the concept of state
/// restoration to provide the illusion that apps continue to run in the
/// background forever: after an app has been backgrounded, the user can always
/// return to it and find it in the same state. In practice, the operating
/// system may, however, terminate the app to free resources for other apps
/// running in the foreground. Before that happens, the app gets a chance to
/// serialize out its restoration data. When the user navigates back to the
/// backgrounded app, it is restarted and the serialized restoration data is
/// provided to it again. Ideally, the app will use that data to restore itself
/// to the same state it was in when the user backgrounded the app.
///
/// In Flutter, restoration data is organized in a tree of [RestorationBucket]s
/// which is rooted in the [rootBucket]. All information that the application
/// needs to restore its current state must be stored in a bucket in this
/// hierarchy. To store data in the hierarchy, entities (e.g. [Widget]s) must
/// claim ownership of a child bucket from a parent bucket (which may be the
/// [rootBucket] provided by this [RestorationManager]). The owner of a bucket
/// may store arbitrary values in the bucket as long as they can be serialized
/// with the [StandardMessageCodec]. The values are stored in the bucket under a
/// given restoration ID as key. A restoration ID is a [String] that must be
/// unique within a given bucket. To access the stored value again during state
/// restoration, the same restoration ID must be provided again. The owner of
/// the bucket may also make the bucket available to other entities so that they
/// can claim child buckets from it for their own restoration needs. Within a
/// bucket, child buckets are also identified by unique restoration IDs. The
/// restoration ID must be provided when claiming a child bucket.
///
/// When restoration data is provided to the [RestorationManager] (e.g. after
/// the application relaunched when foregrounded again), the bucket hierarchy
/// with all the data stored in it is restored. Entities can retrieve the data
/// again by using the same restoration IDs that they originally used to store
/// the data.
///
/// In addition to providing restoration data when the app is launched,
/// restoration data may also be provided to a running app to restore it to a
/// previous state (e.g. when the user hits the back/forward button in the web
/// browser). When this happens, the [RestorationManager] notifies its listeners
/// (added via [addListener]) that a new [rootBucket] is available. In response
/// to the notification, listeners must stop using the old bucket and restore
/// their state from the information in the new [rootBucket].
///
/// Some platforms restrict the size of the restoration data. Therefore, the
/// data stored in the buckets should be as small as possible while still
/// allowing the app to restore its current state from it. Data that can be
/// retrieved from other services (e.g. a database or a web server) should not
/// be included in the restoration data. Instead, a small identifier (e.g. a
/// UUID, database record number, or resource locator) should be stored that can
/// be used to retrieve the data again from its original source during state
/// restoration.
///
/// The [RestorationManager] sends a serialized version of the bucket hierarchy
/// over to the engine at the end of a frame in which the data in the hierarchy
/// or its shape has changed. The engine caches the data until the operating
/// system needs it. The application is responsible for keeping the data in the
/// bucket always up-to-date to reflect its current state.
///
/// ## Discussion
///
/// Due to Flutter's threading model and restrictions in the APIs of the
/// platforms Flutter runs on, restoration data must be stored in the buckets
/// proactively as described above. When the operating system asks for the
/// restoration data, it will do so on the platform thread expecting a
/// synchronous response. To avoid the risk of deadlocks, the platform thread
/// cannot block and call into the UI thread (where the dart code is running) to
/// retrieve the restoration data. For this reason, the [RestorationManager]
/// always sends the latest copy of the restoration data from the UI thread over
/// to the platform thread whenever it changes. That way, the restoration data
/// is always ready to go on the platform thread when the operating system needs
/// it.
///
/// ## State Restoration on iOS
///
/// To enable state restoration on iOS, a restoration identifier has to be
/// assigned to the [FlutterViewController](/ios-embedder/interface_flutter_view_controller.html).
/// If the standard embedding (produced by `flutter create`) is used, this can
/// be accomplished with the following steps:
///
/// 1. In the app's directory, open `ios/Runner.xcodeproj` with Xcode.
/// 2. Select `Main.storyboard` under `Runner/Runner` in the Project Navigator
/// on the left.
/// 3. Select the `Flutter View Controller` under
/// `Flutter View Controller Scene` in the view hierarchy.
/// 4. Navigate to the Identity Inspector in the panel on the right.
/// 5. Enter a unique restoration ID in the provided field.
/// 6. Save the project.
///
/// ## Development with hot restart and hot reload
///
/// Changes applied to your app with hot reload and hot restart are not
/// persisted on the device. They are lost when the app is fully terminated and
/// restarted, e.g. by the operating system. Therefore, your app may not restore
/// correctly during development if you have made changes and applied them with
/// hot restart or hot reload. To test state restoration, always make sure to
/// fully re-compile your application (e.g. by re-executing `flutter run`) after
/// making a change.
///
/// ## Testing State Restoration
///
/// {@template flutter.widgets.RestorationManager}
/// To test state restoration on Android:
/// 1. Turn on "Don't keep activities", which destroys the Android activity
/// as soon as the user leaves it. This option should become available
/// when Developer Options are turned on for the device.
/// 2. Run the code sample on an Android device.
/// 3. Create some in-memory state in the app on the phone,
/// e.g. by navigating to a different screen.
/// 4. Background the Flutter app, then return to it. It will restart
/// and restore its state.
///
/// To test state restoration on iOS:
/// 1. Open `ios/Runner.xcworkspace/` in Xcode.
/// 2. (iOS 14+ only): Switch to build in profile or release mode, as
/// launching an app from the home screen is not supported in debug
/// mode.
/// 2. Press the Play button in Xcode to build and run the app.
/// 3. Create some in-memory state in the app on the phone,
/// e.g. by navigating to a different screen.
/// 4. Background the app on the phone, e.g. by going back to the home screen.
/// 5. Press the Stop button in Xcode to terminate the app while running in
/// the background.
/// 6. Open the app again on the phone (not via Xcode). It will restart
/// and restore its state.
/// {@endtemplate}
///
/// See also:
///
/// * [ServicesBinding.restorationManager], which holds the singleton instance
/// of the [RestorationManager] for the currently running application.
/// * [RestorationBucket], which make up the restoration data hierarchy.
/// * [RestorationMixin], which uses [RestorationBucket]s behind the scenes
/// to make [State] objects of [StatefulWidget]s restorable.
class RestorationManager extends ChangeNotifier {
/// Construct the restoration manager and set up the communications channels
/// with the engine to get restoration messages (by calling [initChannels]).
RestorationManager() {
if (kFlutterMemoryAllocationsEnabled) {
ChangeNotifier.maybeDispatchObjectCreation(this);
}
initChannels();
}
/// Sets up the method call handler for [SystemChannels.restoration].
///
/// This is called by the constructor to configure the communications channel
/// with the Flutter engine to get restoration messages.
///
/// Subclasses (especially in tests) can override this to avoid setting up
/// that communications channel, or to set it up differently, as necessary.
@protected
void initChannels() {
SystemChannels.restoration.setMethodCallHandler(_methodHandler);
}
/// The root of the [RestorationBucket] hierarchy containing the restoration
/// data.
///
/// Child buckets can be claimed from this bucket via
/// [RestorationBucket.claimChild]. If the [RestorationManager] has been asked
/// to restore the application to a previous state, these buckets will contain
/// the previously stored data. Otherwise the root bucket (and all children
/// claimed from it) will be empty.
///
/// The [RestorationManager] informs its listeners (added via [addListener])
/// when the value returned by this getter changes. This happens when new
/// restoration data has been provided to the [RestorationManager] to restore
/// the application to a different state. In response to the notification,
/// listeners must stop using the old root bucket and obtain the new one via
/// this getter ([rootBucket] will have been updated to return the new bucket
/// just before the listeners are notified).
///
/// The restoration data describing the current bucket hierarchy is retrieved
/// asynchronously from the engine the first time the root bucket is accessed
/// via this getter. After the data has been copied over from the engine, this
/// getter will return a [SynchronousFuture], that immediately resolves to the
/// root [RestorationBucket].
///
/// The returned [Future] may resolve to null if state restoration is
/// currently turned off.
///
/// See also:
///
/// * [RootRestorationScope], which makes the root bucket available in the
/// [Widget] tree.
Future<RestorationBucket?> get rootBucket {
if (_rootBucketIsValid) {
return SynchronousFuture<RestorationBucket?>(_rootBucket);
}
if (_pendingRootBucket == null) {
_pendingRootBucket = Completer<RestorationBucket?>();
_getRootBucketFromEngine();
}
return _pendingRootBucket!.future;
}
RestorationBucket? _rootBucket; // May be null to indicate that restoration is turned off.
Completer<RestorationBucket?>? _pendingRootBucket;
bool _rootBucketIsValid = false;
/// Returns true for the frame after [rootBucket] has been replaced with a
/// new non-null bucket.
///
/// When true, entities should forget their current state and restore
/// their state according to the information in the new [rootBucket].
///
/// The [RestorationManager] informs its listeners (added via [addListener])
/// when this flag changes from false to true.
bool get isReplacing => _isReplacing;
bool _isReplacing = false;
Future<void> _getRootBucketFromEngine() async {
final Map<Object?, Object?>? config = await SystemChannels.restoration
.invokeMethod<Map<Object?, Object?>>('get');
if (_pendingRootBucket == null) {
// The restoration data was obtained via other means (e.g. by calling
// [handleRestorationDataUpdate] while the request to the engine was
// outstanding. Ignore the engine's response.
return;
}
assert(_rootBucket == null);
_parseAndHandleRestorationUpdateFromEngine(config);
}
void _parseAndHandleRestorationUpdateFromEngine(Map<Object?, Object?>? update) {
handleRestorationUpdateFromEngine(
enabled: update != null && update['enabled']! as bool,
data: update == null ? null : update['data'] as Uint8List?,
);
}
/// Called by the [RestorationManager] on itself to parse the restoration
/// information obtained from the engine.
///
/// The `enabled` parameter indicates whether the engine wants to receive
/// restoration data. When `enabled` is false, state restoration is turned
/// off and the [rootBucket] is set to null. When `enabled` is true, the
/// provided restoration `data` will be parsed into a new [rootBucket]. If
/// `data` is null, an empty [rootBucket] will be instantiated.
///
/// Subclasses in test frameworks may call this method at any time to inject
/// restoration data (obtained e.g. by overriding [sendToEngine]) into the
/// [RestorationManager]. When the method is called before the [rootBucket] is
/// accessed, [rootBucket] will complete synchronously the next time it is
/// called.
@protected
void handleRestorationUpdateFromEngine({required bool enabled, required Uint8List? data}) {
assert(enabled || data == null);
_isReplacing = _rootBucketIsValid && enabled;
if (_isReplacing) {
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
_isReplacing = false;
}, debugLabel: 'RestorationManager.resetIsReplacing');
}
final RestorationBucket? oldRoot = _rootBucket;
_rootBucket = enabled
? RestorationBucket.root(manager: this, rawData: _decodeRestorationData(data))
: null;
_rootBucketIsValid = true;
assert(_pendingRootBucket == null || !_pendingRootBucket!.isCompleted);
_pendingRootBucket?.complete(_rootBucket);
_pendingRootBucket = null;
if (_rootBucket != oldRoot) {
notifyListeners();
oldRoot?.dispose();
}
}
/// Called by the [RestorationManager] on itself to send the provided
/// encoded restoration data to the engine.
///
/// The `encodedData` describes the entire bucket hierarchy that makes up the
/// current restoration data.
///
/// Subclasses in test frameworks may override this method to capture the
/// restoration data that would have been send to the engine. The captured
/// data can be re-injected into the [RestorationManager] via the
/// [handleRestorationUpdateFromEngine] method to restore the state described
/// by the data.
@protected
Future<void> sendToEngine(Uint8List encodedData) {
return SystemChannels.restoration.invokeMethod<void>('put', encodedData);
}
Future<void> _methodHandler(MethodCall call) async {
switch (call.method) {
case 'push':
_parseAndHandleRestorationUpdateFromEngine(call.arguments as Map<Object?, Object?>);
default:
throw UnimplementedError(
"${call.method} was invoked but isn't implemented by $runtimeType",
);
}
}
Map<Object?, Object?>? _decodeRestorationData(Uint8List? data) {
if (data == null) {
return null;
}
final ByteData encoded = data.buffer.asByteData(data.offsetInBytes, data.lengthInBytes);
return const StandardMessageCodec().decodeMessage(encoded) as Map<Object?, Object?>?;
}
Uint8List _encodeRestorationData(Map<Object?, Object?> data) {
final ByteData encoded = const StandardMessageCodec().encodeMessage(data)!;
return encoded.buffer.asUint8List(encoded.offsetInBytes, encoded.lengthInBytes);
}
bool _debugDoingUpdate = false;
bool _serializationScheduled = false;
final Set<RestorationBucket> _bucketsNeedingSerialization = <RestorationBucket>{};
/// Called by a [RestorationBucket] to request serialization for that bucket.
///
/// This method is called by a bucket in the hierarchy whenever the data
/// in it or the shape of the hierarchy has changed.
///
/// Calling this is a no-op when the bucket is already scheduled for
/// serialization.
///
/// It is exposed to allow testing of [RestorationBucket]s in isolation.
@protected
@visibleForTesting
void scheduleSerializationFor(RestorationBucket bucket) {
assert(bucket._manager == this);
assert(!_debugDoingUpdate);
_bucketsNeedingSerialization.add(bucket);
if (!_serializationScheduled) {
_serializationScheduled = true;
SchedulerBinding.instance.addPostFrameCallback(
(Duration _) => _doSerialization(),
debugLabel: 'RestorationManager.doSerialization',
);
}
}
/// Called by a [RestorationBucket] to unschedule a request for serialization.
///
/// This method is called by a bucket in the hierarchy whenever it no longer
/// needs to be serialized (e.g. because the bucket got disposed).
///
/// It is safe to call this even when the bucket wasn't scheduled for
/// serialization before.
///
/// It is exposed to allow testing of [RestorationBucket]s in isolation.
@protected
@visibleForTesting
void unscheduleSerializationFor(RestorationBucket bucket) {
assert(bucket._manager == this);
assert(!_debugDoingUpdate);
_bucketsNeedingSerialization.remove(bucket);
}
void _doSerialization() {
if (!_serializationScheduled) {
return;
}
assert(() {
_debugDoingUpdate = true;
return true;
}());
_serializationScheduled = false;
for (final RestorationBucket bucket in _bucketsNeedingSerialization) {
bucket.finalize();
}
_bucketsNeedingSerialization.clear();
sendToEngine(_encodeRestorationData(_rootBucket!._rawData));
assert(() {
_debugDoingUpdate = false;
return true;
}());
}
/// Called to manually flush the restoration data to the engine.
///
/// A change in restoration data is usually accompanied by scheduling a frame
/// (because the restoration data is modified inside a [State.setState] call,
/// because it is usually something that affects the interface). Restoration
/// data is automatically flushed to the engine at the end of a frame. As a
/// result, it is uncommon to need to call this method directly. However, if
/// restoration data is changed without triggering a frame, this method must
/// be called to ensure that the updated restoration data is sent to the
/// engine in a timely manner. An example of such a use case is the
/// [Scrollable], where the final scroll offset after a scroll activity
/// finishes is determined between frames without scheduling a new frame.
///
/// Calling this method is a no-op if a frame is already scheduled. In that
/// case, the restoration data will be flushed to the engine at the end of
/// that frame. If this method is called and no frame is scheduled, the
/// current restoration data is directly sent to the engine.
void flushData() {
assert(!_debugDoingUpdate);
if (SchedulerBinding.instance.hasScheduledFrame) {
return;
}
_doSerialization();
assert(!_serializationScheduled);
}
@override
void dispose() {
_rootBucket?.dispose();
super.dispose();
}
}
/// A [RestorationBucket] holds pieces of the restoration data that a part of
/// the application needs to restore its state.
///
/// For a general overview of how state restoration works in Flutter, see the
/// [RestorationManager].
///
/// [RestorationBucket]s are organized in a tree that is rooted in
/// [RestorationManager.rootBucket] and managed by a [RestorationManager]. The
/// tree is serializable and must contain all the data an application needs to
/// restore its current state at a later point in time.
///
/// A [RestorationBucket] stores restoration data as key-value pairs. The key is
/// a [String] representing a restoration ID that identifies a piece of data
/// uniquely within a bucket. The value can be anything that is serializable via
/// the [StandardMessageCodec]. Furthermore, a [RestorationBucket] may have
/// child buckets, which are identified within their parent via a unique
/// restoration ID as well.
///
/// During state restoration, the data previously stored in the
/// [RestorationBucket] hierarchy will be made available again to the
/// application to restore it to the state it had when the data was collected.
/// State restoration to a previous state may happen when the app is launched
/// (e.g. after it has been terminated gracefully while running in the
/// background) or after the app has already been running for a while.
///
/// ## Lifecycle
///
/// A [RestorationBucket] is rarely instantiated directly via its constructors.
/// Instead, when an entity wants to store data in or retrieve data from a
/// restoration bucket, it typically obtains a child bucket from a parent by
/// calling [claimChild]. If no parent is available,
/// [RestorationManager.rootBucket] may be used as a parent. When claiming a
/// child, the claimer must provide the restoration ID of the child it would
/// like to own. A child bucket with a given restoration ID can at most have
/// one owner. If another owner tries to claim a bucket with the same ID from
/// the same parent, an exception is thrown (see discussion in [claimChild]).
/// The restoration IDs that a given owner uses to claim a child (and to store
/// data in that child, see below) must be stable across app launches to ensure
/// that after the app restarts the owner can retrieve the same data again that
/// it stored during a previous run.
///
/// Per convention, the owner of the bucket has exclusive access to the values
/// stored in the bucket. It can read, add, modify, and remove values via the
/// [read], [write], and [remove] methods. In general, the owner should store
/// all the data in the bucket that it needs to restore its current state. If
/// its current state changes, the data in the bucket must be updated. At the
/// same time, the data in the bucket should be kept to a minimum. For example,
/// for data that can be retrieved from other sources (like a database or
/// web service) only enough information (e.g. an ID or resource locator) to
/// re-obtain that data should be stored in the bucket. In addition to managing
/// the data in a bucket, an owner may also make the bucket available to other
/// entities so they can claim child buckets from it via [claimChild] for their
/// own restoration needs.
///
/// The bucket returned by [claimChild] may either contain state information
/// that the owner had previously (e.g. during a previous run of the
/// application) stored in it or it may be empty. If the bucket contains data,
/// the owner is expected to restore its state with the information previously
/// stored in the bucket. If the bucket is empty, it may initialize itself to
/// default values.
///
/// When the data stored in a bucket is no longer needed to restore the
/// application to its current state (e.g. because the owner of the bucket is no
/// longer shown on screen), the bucket must be [dispose]d. This will remove all
/// information stored in the bucket from the app's restoration data and that
/// information will not be available again when the application is restored to
/// this state in the future.
class RestorationBucket {
/// Creates an empty [RestorationBucket] to be provided to [adoptChild] to add
/// it to the bucket hierarchy.
///
/// {@template flutter.services.RestorationBucket.empty.bucketCreation}
/// Instantiating a bucket directly is rare, most buckets are created by
/// claiming a child from a parent via [claimChild]. If no parent bucket is
/// available, [RestorationManager.rootBucket] may be used as a parent.
/// {@endtemplate}
RestorationBucket.empty({required String restorationId, required Object? debugOwner})
: _restorationId = restorationId,
_rawData = <String, Object?>{} {
assert(() {
_debugOwner = debugOwner;
return true;
}());
assert(debugMaybeDispatchCreated('services', 'RestorationBucket', this));
}
/// Creates the root [RestorationBucket] for the provided restoration
/// `manager`.
///
/// The `rawData` must either be null (in which case an empty bucket will be
/// instantiated) or it must be a nested map describing the entire bucket
/// hierarchy in the following format:
///
/// ```javascript
/// {
/// 'v': { // key-value pairs
/// // * key is a string representation a restoration ID
/// // * value is any primitive that can be encoded with [StandardMessageCodec]
/// '<restoration-id>: <Object>,
/// },
/// 'c': { // child buckets
/// 'restoration-id': <nested map representing a child bucket>
/// }
/// }
/// ```
///
/// {@macro flutter.services.RestorationBucket.empty.bucketCreation}
RestorationBucket.root({
required RestorationManager manager,
required Map<Object?, Object?>? rawData,
}) : _manager = manager,
_rawData = rawData ?? <Object?, Object?>{},
_restorationId = 'root' {
assert(() {
_debugOwner = manager;
return true;
}());
assert(debugMaybeDispatchCreated('services', 'RestorationBucket', this));
}
/// Creates a child bucket initialized with the data that the provided
/// `parent` has stored under the provided [restorationId].
///
/// This constructor cannot be used if the `parent` does not have any child
/// data stored under the given ID. In that case, create an empty bucket (via
/// [RestorationBucket.empty] and have the parent adopt it via [adoptChild].
///
/// {@macro flutter.services.RestorationBucket.empty.bucketCreation}
RestorationBucket.child({
required String restorationId,
required RestorationBucket parent,
required Object? debugOwner,
}) : assert(parent._rawChildren[restorationId] != null),
_manager = parent._manager,
_parent = parent,
_rawData = parent._rawChildren[restorationId]! as Map<Object?, Object?>,
_restorationId = restorationId {
assert(() {
_debugOwner = debugOwner;
return true;
}());
assert(debugMaybeDispatchCreated('services', 'RestorationBucket', this));
}
static const String _childrenMapKey = 'c';
static const String _valuesMapKey = 'v';
final Map<Object?, Object?> _rawData;
/// The owner of the bucket that was provided when the bucket was claimed via
/// [claimChild].
///
/// The value is used in error messages. Accessing the value is only valid
/// in debug mode, otherwise it will return null.
Object? get debugOwner {
assert(_debugAssertNotDisposed());
return _debugOwner;
}
Object? _debugOwner;
RestorationManager? _manager;
RestorationBucket? _parent;
/// Returns true when entities processing this bucket should restore their
/// state from the information in the bucket (e.g. via [read] and
/// [claimChild]) instead of copying their current state information into the
/// bucket (e.g. via [write] and [adoptChild].
///
/// This flag is true for the frame after the [RestorationManager] has been
/// instructed to restore the application from newly provided restoration
/// data.
bool get isReplacing => _manager?.isReplacing ?? false;
/// The restoration ID under which the bucket is currently stored in the
/// parent of this bucket (or wants to be stored if it is currently
/// parent-less).
///
/// This value is never null.
String get restorationId {
assert(_debugAssertNotDisposed());
return _restorationId;
}
String _restorationId;
// Maps a restoration ID to the raw map representation of a child bucket.
Map<Object?, Object?> get _rawChildren =>
_rawData.putIfAbsent(_childrenMapKey, () => <Object?, Object?>{})! as Map<Object?, Object?>;
// Maps a restoration ID to a value that is stored in this bucket.
Map<Object?, Object?> get _rawValues =>
_rawData.putIfAbsent(_valuesMapKey, () => <Object?, Object?>{})! as Map<Object?, Object?>;
// Get and store values.
/// Returns the value of type `P` that is currently stored in the bucket under
/// the provided `restorationId`.
///
/// Returns null if nothing is stored under that id. Throws, if the value
/// stored under the ID is not of type `P`.
///
/// See also:
///
/// * [write], which stores a value in the bucket.
/// * [remove], which removes a value from the bucket.
/// * [contains], which checks whether any value is stored under a given
/// restoration ID.
P? read<P>(String restorationId) {
assert(_debugAssertNotDisposed());
return _rawValues[restorationId] as P?;
}
/// Stores the provided `value` of type `P` under the provided `restorationId`
/// in the bucket.
///
/// Any value that has previously been stored under that ID is overwritten
/// with the new value. The provided `value` must be serializable with the
/// [StandardMessageCodec].
///
/// Null values will be stored in the bucket as-is. To remove a value, use
/// [remove].
///
/// See also:
///
/// * [read], which retrieves a stored value from the bucket.
/// * [remove], which removes a value from the bucket.
/// * [contains], which checks whether any value is stored under a given
/// restoration ID.
void write<P>(String restorationId, P value) {
assert(_debugAssertNotDisposed());
assert(debugIsSerializableForRestoration(value));
if (_rawValues[restorationId] != value || !_rawValues.containsKey(restorationId)) {
_rawValues[restorationId] = value;
_markNeedsSerialization();
}
}
/// Deletes the value currently stored under the provided `restorationId` from
/// the bucket.
///
/// The value removed from the bucket is casted to `P` and returned. If no
/// value was stored under that id, null is returned.
///
/// See also:
///
/// * [read], which retrieves a stored value from the bucket.
/// * [write], which stores a value in the bucket.
/// * [contains], which checks whether any value is stored under a given
/// restoration ID.
P? remove<P>(String restorationId) {
assert(_debugAssertNotDisposed());
final bool needsUpdate = _rawValues.containsKey(restorationId);
final result = _rawValues.remove(restorationId) as P?;
if (_rawValues.isEmpty) {
_rawData.remove(_valuesMapKey);
}
if (needsUpdate) {
_markNeedsSerialization();
}
return result;
}
/// Checks whether a value stored in the bucket under the provided
/// `restorationId`.
///
/// See also:
///
/// * [read], which retrieves a stored value from the bucket.
/// * [write], which stores a value in the bucket.
/// * [remove], which removes a value from the bucket.
bool contains(String restorationId) {
assert(_debugAssertNotDisposed());
return _rawValues.containsKey(restorationId);
}
// Child management.
// The restoration IDs and associated buckets of children that have been
// claimed via [claimChild].
final Map<String, RestorationBucket> _claimedChildren = <String, RestorationBucket>{};
// Newly created child buckets whose restoration ID is still in use, see
// comment in [claimChild] for details.
final Map<String, List<RestorationBucket>> _childrenToAdd = <String, List<RestorationBucket>>{};
/// Claims ownership of the child with the provided `restorationId` from this
/// bucket.
///
/// If the application is getting restored to a previous state, the bucket
/// will contain all the data that was previously stored in the bucket.
/// Otherwise, an empty bucket is returned.
///
/// The claimer of the bucket is expected to use the data stored in the bucket
/// to restore itself to its previous state described by the data in the
/// bucket. If the bucket is empty, it should initialize itself to default
/// values. Whenever the information that the claimer needs to restore its
/// state changes, the data in the bucket should be updated to reflect that.
///
/// A child bucket with a given `restorationId` can only have one owner. If
/// another owner claims a child bucket with the same `restorationId` an
/// exception will be thrown at the end of the current frame unless the
/// previous owner has either deleted its bucket by calling [dispose] or has
/// moved it to a new parent via [adoptChild].
///
/// When the returned bucket is no longer needed, it must be [dispose]d to
/// delete the information stored in it from the app's restoration data.
RestorationBucket claimChild(String restorationId, {required Object? debugOwner}) {
assert(_debugAssertNotDisposed());
// There are three cases to consider:
// 1. Claiming an ID that has already been claimed.
// 2. Claiming an ID that doesn't yet exist in [_rawChildren].
// 3. Claiming an ID that does exist in [_rawChildren] and hasn't been
// claimed yet.
// If an ID has already been claimed (case 1) the current owner may give up
// that ID later this frame and it can be re-used. In anticipation of the
// previous owner's surrender of the id, we return an empty bucket for this
// new claim and check in [_debugAssertIntegrity] that at the end of the
// frame the old owner actually did surrendered the id.
// Case 2 also requires the creation of a new empty bucket.
// In Case 3 we create a new bucket wrapping the existing data in
// [_rawChildren].
// Case 1+2: Adopt and return an empty bucket.
if (_claimedChildren.containsKey(restorationId) || !_rawChildren.containsKey(restorationId)) {
final child = RestorationBucket.empty(debugOwner: debugOwner, restorationId: restorationId);
adoptChild(child);
return child;
}
// Case 3: Return bucket wrapping the existing data.
assert(_rawChildren[restorationId] != null);
final child = RestorationBucket.child(
restorationId: restorationId,
parent: this,
debugOwner: debugOwner,
);
_claimedChildren[restorationId] = child;
return child;
}
/// Adopts the provided `child` bucket.
///
/// The `child` will be dropped from its old parent, if it had one.
///
/// The `child` is stored under its [restorationId] in this bucket. If this
/// bucket already contains a child bucket under the same id, the owner of
/// that existing bucket must give it up (e.g. by moving the child bucket to a
/// different parent or by disposing it) before the end of the current frame.
/// Otherwise an exception indicating the illegal use of duplicated
/// restoration IDs will trigger in debug mode.
///
/// No-op if the provided bucket is already a child of this bucket.
void adoptChild(RestorationBucket child) {
assert(_debugAssertNotDisposed());
if (child._parent != this) {
child._parent?._removeChildData(child);
child._parent = this;
_addChildData(child);
if (child._manager != _manager) {
_recursivelyUpdateManager(child);
}
}
assert(child._parent == this);
assert(child._manager == _manager);
}
void _dropChild(RestorationBucket child) {
assert(child._parent == this);
_removeChildData(child);
child._parent = null;
if (child._manager != null) {
child._updateManager(null);
child._visitChildren(_recursivelyUpdateManager);
}
}
bool _needsSerialization = false;
void _markNeedsSerialization() {
if (!_needsSerialization) {
_needsSerialization = true;
_manager?.scheduleSerializationFor(this);
}
}
/// Called by the [RestorationManager] just before the data of the bucket
/// is serialized and send to the engine.
///
/// It is exposed to allow testing of [RestorationBucket]s in isolation.
@visibleForTesting
void finalize() {
assert(_debugAssertNotDisposed());
assert(_needsSerialization);
_needsSerialization = false;
assert(_debugAssertIntegrity());
}
void _recursivelyUpdateManager(RestorationBucket bucket) {
bucket._updateManager(_manager);
bucket._visitChildren(_recursivelyUpdateManager);
}
void _updateManager(RestorationManager? newManager) {
if (_manager == newManager) {
return;
}
if (_needsSerialization) {
_manager?.unscheduleSerializationFor(this);
}
_manager = newManager;
if (_needsSerialization && _manager != null) {
_needsSerialization = false;
_markNeedsSerialization();
}
}
bool _debugAssertIntegrity() {
assert(() {
if (_childrenToAdd.isEmpty) {
return true;
}
final error = <DiagnosticsNode>[
ErrorSummary('Multiple owners claimed child RestorationBuckets with the same IDs.'),
ErrorDescription('The following IDs were claimed multiple times from the parent $this:'),
];
for (final MapEntry<String, List<RestorationBucket>> child in _childrenToAdd.entries) {
final String id = child.key;
final List<RestorationBucket> buckets = child.value;
assert(buckets.isNotEmpty);
assert(_claimedChildren.containsKey(id));
error.addAll(<DiagnosticsNode>[
ErrorDescription(' * "$id" was claimed by:'),
...buckets.map(
(RestorationBucket bucket) => ErrorDescription(' * ${bucket.debugOwner}'),
),
ErrorDescription(' * ${_claimedChildren[id]!.debugOwner} (current owner)'),
]);
}
throw FlutterError.fromParts(error);
}());
return true;
}
void _removeChildData(RestorationBucket child) {
assert(child._parent == this);
if (_claimedChildren.remove(child.restorationId) == child) {
_rawChildren.remove(child.restorationId);
final List<RestorationBucket>? pendingChildren = _childrenToAdd[child.restorationId];
if (pendingChildren != null) {
final RestorationBucket toAdd = pendingChildren.removeLast();
_finalizeAddChildData(toAdd);
if (pendingChildren.isEmpty) {
_childrenToAdd.remove(child.restorationId);
}
}
if (_rawChildren.isEmpty) {
_rawData.remove(_childrenMapKey);
}
_markNeedsSerialization();
return;
}
_childrenToAdd[child.restorationId]?.remove(child);
if (_childrenToAdd[child.restorationId]?.isEmpty ?? false) {
_childrenToAdd.remove(child.restorationId);
}
}
void _addChildData(RestorationBucket child) {
assert(child._parent == this);
if (_claimedChildren.containsKey(child.restorationId)) {
// Delay addition until the end of the frame in the hopes that the current
// owner of the child with the same ID will have given up that child by
// then.
_childrenToAdd.putIfAbsent(child.restorationId, () => <RestorationBucket>[]).add(child);
_markNeedsSerialization();
return;
}
_finalizeAddChildData(child);
_markNeedsSerialization();
}
void _finalizeAddChildData(RestorationBucket child) {
assert(_claimedChildren[child.restorationId] == null);
assert(_rawChildren[child.restorationId] == null);
_claimedChildren[child.restorationId] = child;
_rawChildren[child.restorationId] = child._rawData;
}
void _visitChildren(_BucketVisitor visitor, {bool concurrentModification = false}) {
Iterable<RestorationBucket> children = _claimedChildren.values.followedBy(
_childrenToAdd.values.expand((List<RestorationBucket> buckets) => buckets),
);
if (concurrentModification) {
children = children.toList(growable: false);
}
children.forEach(visitor);
}
// Bucket management
/// Changes the restoration ID under which the bucket is (or will be) stored
/// in its parent to `newRestorationId`.
///
/// No-op if the bucket is already stored under the provided id.
///
/// If another owner has already claimed a bucket with the provided `newId` an
/// exception will be thrown at the end of the current frame unless the other
/// owner has deleted its bucket by calling [dispose], [rename]ed it using
/// another ID, or has moved it to a new parent via [adoptChild].
void rename(String newRestorationId) {
assert(_debugAssertNotDisposed());
if (newRestorationId == restorationId) {
return;
}
_parent?._removeChildData(this);
_restorationId = newRestorationId;
_parent?._addChildData(this);
}
/// Deletes the bucket and all the data stored in it from the bucket
/// hierarchy.
///
/// After [dispose] has been called, the data stored in this bucket and its
/// children are no longer part of the app's restoration data. The data
/// originally stored in the bucket will not be available again when the
/// application is restored to this state in the future. It is up to the
/// owners of the children to either move them (via [adoptChild]) to a new
/// parent that is still part of the bucket hierarchy or to [dispose] of them
/// as well.
///
/// This method must only be called by the object's owner.
void dispose() {
assert(_debugAssertNotDisposed());
assert(debugMaybeDispatchDisposed(this));
_visitChildren(_dropChild, concurrentModification: true);
_claimedChildren.clear();
_childrenToAdd.clear();
_parent?._removeChildData(this);
_parent = null;
_updateManager(null);
_debugDisposed = true;
}
@override
String toString() =>
'${objectRuntimeType(this, 'RestorationBucket')}(restorationId: $restorationId, owner: $debugOwner)';
bool _debugDisposed = false;
bool _debugAssertNotDisposed() {
assert(() {
if (_debugDisposed) {
throw FlutterError(
'A $runtimeType was used after being disposed.\n'
'Once you have called dispose() on a $runtimeType, it can no longer be used.',
);
}
return true;
}());
return true;
}
}
/// Returns true when the provided `object` is serializable for state
/// restoration.
///
/// Should only be called from within asserts. Always returns false outside