├── image.png ├── lib ├── updated.dart └── src │ ├── notifier.dart │ ├── future.dart │ └── update.dart ├── .gitignore ├── pubspec.yaml ├── analysis_options.yaml ├── CHANGELOG.md ├── LICENSE ├── example └── example.dart ├── README.md └── test └── future_test.dart /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aloisdeniel/updated/HEAD/image.png -------------------------------------------------------------------------------- /lib/updated.dart: -------------------------------------------------------------------------------- 1 | library updated; 2 | 3 | export 'src/update.dart'; 4 | export 'src/notifier.dart'; 5 | export 'src/future.dart'; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | 5 | # Omit commiting pubspec.lock for library packages: 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock 7 | pubspec.lock 8 | 9 | # Conventional directory for build outputs 10 | build/ 11 | 12 | # Directory created by dartdoc 13 | doc/api/ 14 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: updated 2 | description: Managing state of asynchronous updates as a simple dart representation. 3 | version: 0.1.10 4 | homepage: https://github.com/aloisdeniel/updated 5 | 6 | environment: 7 | sdk: '>=2.7.0 <3.0.0' 8 | 9 | dependencies: 10 | meta: ^1.2.4 11 | 12 | dev_dependencies: 13 | pedantic: ^1.9.0 14 | test: ^1.14.4 15 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:pedantic/analysis_options.yaml 5 | 6 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 7 | # Uncomment to specify additional rules. 8 | # linter: 9 | # rules: 10 | # - camel_case_types 11 | 12 | analyzer: 13 | # exclude: 14 | # - path/to/excluded/files/** 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.10 2 | 3 | - Added `mapUpdate` method. 4 | 5 | ## 0.1.9 6 | 7 | - Added `Update.combine` function. 8 | 9 | ## 0.1.7 10 | 11 | - Fixed `expirationDate`. 12 | 13 | ## 0.1.6 14 | 15 | - Added `expirationDate` and `expirationDuration`. 16 | 17 | ## 0.1.5 18 | 19 | - Fixed `maybeMap`. 20 | 21 | ## 0.1.4 22 | 23 | - Added `maybeMap` and `hasSucceeded` to `Update`. 24 | 25 | ## 0.1.3 26 | 27 | - Added default constructor for `Updated`. 28 | 29 | ## 0.1.2 30 | 31 | - Added the `UpdateNotifier`. 32 | 33 | ## 0.1.1 34 | 35 | - Updated documentation. 36 | 37 | ## 0.1.0 38 | 39 | - Initial version 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aloïs Deniel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'package:updated/updated.dart'; 2 | 3 | Future requestValue() async { 4 | await Future.delayed(const Duration(seconds: 1)); 5 | return 42; 6 | } 7 | 8 | Future main() async { 9 | var value = Update(); 10 | 11 | // --- 12 | // Value : 32 13 | // Is Loading... 14 | // Details : Updating(id: 28313824297, startedAt: 2020-11-23 16:57:04.298751, optimisticValue: 32) 15 | // --- 16 | // Value : 42 17 | // Is not loading : Updated(id: 28313824297, updatedAt: 2020-11-23 16:57:05.315, previousUpdate: 42) 18 | // Details : Updated(id: 28313824297, updatedAt: 2020-11-23 16:57:05.315, previousUpdate: 42) 19 | await for (var item in update( 20 | updater: requestValue, 21 | getUpdate: () => value, 22 | optimisticValue: 32, 23 | )) { 24 | print('---'); 25 | value = item; 26 | item.mapValue( 27 | value: (value, isOptimistic) => print('Value : $value'), 28 | orElse: () => print('No value'), 29 | ); 30 | item.mapLoading( 31 | loading: () => print('Is Loading...'), 32 | notLoading: () => print('Is not loading : $value'), 33 | ); 34 | print('Details : $item'); 35 | } 36 | 37 | // --- 38 | // Value : 32 39 | // Is Loading... 40 | // Details : Refreshing(id: 28313797308, startedAt: 2020-11-23 16:56:38.327266, previousUpdate: Updated(id: 28313797307, updatedAt: 2020-11-23 16:56:38.322668, previousUpdate: 42), optimisticValue: 32) 41 | // --- 42 | // Value : 42 43 | // Is not loading : Updated(id: 28313797308, updatedAt: 2020-11-23 16:56:39.334887, previousUpdate: 42) 44 | // Details : Updated(id: 28313797308, updatedAt: 2020-11-23 16:56:39.334887, previousUpdate: 42) 45 | await for (var item in update( 46 | updater: requestValue, 47 | getUpdate: () => value, 48 | optimisticValue: 32, 49 | )) { 50 | print('---'); 51 | value = item; 52 | item.mapValue( 53 | value: (value, isOptimistic) => print('Value : $value'), 54 | orElse: () => print('No value'), 55 | ); 56 | item.mapLoading( 57 | loading: () => print('Is Loading...'), 58 | notLoading: () => print('Is not loading : $value'), 59 | ); 60 | print('Details : $item'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/notifier.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import 'future.dart' as future; 6 | import 'update.dart'; 7 | 8 | /// A notifier is a store for a given [Update]. 9 | /// 10 | /// The [update] is updated by calling [execute]. 11 | /// 12 | /// If you're using patterns base on immutable data (like redux, MVU), using `update` function directly 13 | /// may be more appropriate. 14 | class UpdateNotifier { 15 | /// Create a new notifier. 16 | /// 17 | /// If an [initialValue] is given, the [update] is initialized as an [Updated], else 18 | /// the value is initialized as a [NotUpdated]. 19 | UpdateNotifier({ 20 | T initialValue, 21 | }) : _update = initialValue == null 22 | ? Update() 23 | : Updated.fromUpdating( 24 | Updating.fromNotLoaded( 25 | NotLoaded(), 26 | id: 0, 27 | ), 28 | initialValue, 29 | ); 30 | 31 | /// Indicates a new change of [update]. 32 | /// 33 | /// Changes are emmited by calling the [execute] method. 34 | Stream> get updateChanged => _updateChanged.stream; 35 | 36 | /// Get the current value. 37 | Update get update => _update; 38 | 39 | final StreamController> _updateChanged = 40 | StreamController>.broadcast(); 41 | 42 | Update _update; 43 | 44 | /// Starts a sequence of updates by running the [updater] and raising a new [Update] after each step of its execution. 45 | /// 46 | /// If an update has already been started from the current [update], then the behaviour is controlled 47 | /// by the [override] parameters. The new update can whether be ignore, or cancel the previous execution. 48 | /// 49 | /// An [optimisticValue] can be given to display an anticipated result during the loading phase. 50 | void execute({ 51 | @required Future Function() updater, 52 | future.UpdateOverride override = future.UpdateOverride.ignore, 53 | T optimisticValue, 54 | }) async { 55 | await for (var item in future.update( 56 | getUpdate: () => _update, 57 | updater: updater, 58 | override: override, 59 | optimisticValue: optimisticValue, 60 | )) { 61 | _update = item; 62 | _updateChanged.add(item); 63 | } 64 | } 65 | 66 | /// Release all resource (like stream listeners). 67 | void dispose() { 68 | _updateChanged.close(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Updated 2 | 3 | Managing state of asynchronous updates as a simple dart representation. 4 | 5 | ## Quickstart 6 | 7 | A simple usage example of the `Update` state and `update` method. 8 | 9 | ```dart 10 | import 'package:updated/updated.dart'; 11 | 12 | Future requestValue() async { 13 | await Future.delayed(const Duration(seconds: 1)); 14 | return 42; 15 | } 16 | 17 | Future main() async { 18 | var value = Update(); 19 | 20 | // --- 21 | // Value : 32 22 | // Is Loading... 23 | // Details : Updating(id: 28313824297, startedAt: 2020-11-23 16:57:04.298751, optimisticValue: 32) 24 | // --- 25 | // Value : 42 26 | // Is not loading : Updated(id: 28313824297, updatedAt: 2020-11-23 16:57:05.315, previousUpdate: 42) 27 | // Details : Updated(id: 28313824297, updatedAt: 2020-11-23 16:57:05.315, previousUpdate: 42) 28 | await for (var item in update( 29 | updater: requestValue, 30 | getUpdate: () => value, 31 | optimisticValue: 32, 32 | )) { 33 | print('---'); 34 | value = item; 35 | item.mapValue( 36 | value: (value, isOptimistic) => print('Value : $value'), 37 | orElse: () => print('No value'), 38 | ); 39 | item.mapLoading( 40 | loading: () => print('Is Loading...'), 41 | notLoading: () => print('Is not loading : $value'), 42 | ); 43 | print('Details : $item'); 44 | } 45 | 46 | // --- 47 | // Value : 32 48 | // Is Loading... 49 | // Details : Refreshing(id: 28313797308, startedAt: 2020-11-23 16:56:38.327266, previousUpdate: Updated(id: 28313797307, updatedAt: 2020-11-23 16:56:38.322668, previousUpdate: 42), optimisticValue: 32) 50 | // --- 51 | // Value : 42 52 | // Is not loading : Updated(id: 28313797308, updatedAt: 2020-11-23 16:56:39.334887, previousUpdate: 42) 53 | // Details : Updated(id: 28313797308, updatedAt: 2020-11-23 16:56:39.334887, previousUpdate: 42) 54 | await for (var item in update( 55 | updater: requestValue, 56 | getUpdate: () => value, 57 | optimisticValue: 32, 58 | )) { 59 | print('---'); 60 | value = item; 61 | item.mapValue( 62 | value: (value, isOptimistic) => print('Value : $value'), 63 | orElse: () => print('No value'), 64 | ); 65 | item.mapLoading( 66 | loading: () => print('Is Loading...'), 67 | notLoading: () => print('Is not loading : $value'), 68 | ); 69 | print('Details : $item'); 70 | } 71 | } 72 | ``` 73 | 74 | ## Usage 75 | 76 | An update represents the lifecycle of a `T` value that can be loaded asynchronously. 77 | 78 | ![lifecycle](image.png) 79 | 80 | Its initial state is `NotLoaded`. During its first update, it goes through the `Updating` state then `Updated` or `FailedUpdate` states, whether it is a success or not. During the following updates, it goes through the `Refreshing` state then `Updated` or `FailedRefresh` states, whether it is a success or not. 81 | 82 | #### Initialization 83 | 84 | To create an updated property, create an `Update`. 85 | 86 | ```dart 87 | final property = Update(); 88 | ``` 89 | 90 | #### Updating 91 | 92 | The easiest way to update your `Update` instance is by using the `update` function that returns a `Stream` with the sequence of updates. 93 | 94 | ```dart 95 | var value = Update(); 96 | 97 | final updateStream = update( 98 | // The actual async operation 99 | updater: () async { 100 | // your async calls 101 | }, 102 | // Make sure to always return the current value. This makes cancellation possible. 103 | getUpdate: () => value, 104 | // This value will be available durint loading states 105 | optimisticValue: 32, 106 | // To change the behaviour when an update is triggered but the previous isn't finished yet. 107 | // If a previous update is cancelled then its stream is closed an no more events are emitted. 108 | override: UpdateOverride.cancelPrevious, 109 | )); 110 | ``` 111 | 112 | > If an `optimisticValue` is given, then its value is returned by the `Update.mapValue` method. This may be useful to display the result to the user beforehand if we're able to anticipate it (for example, by liking a tweet on twitter, we can fill the heart before receiving the result of the request that validates it). 113 | 114 | > If a new update is triggered with the `cancelPrevious` option, then the previous update stream won't emit its final updates. 115 | 116 | #### Mapping result 117 | 118 | You can map the current state of an `Update` instance with the `map`, `mapValue`, `mapLoading`, `mapError` methods. 119 | 120 | ```dart 121 | @override 122 | Widget build(BuildContext context) 123 | return state.products.mapValue( 124 | value: (value, isOptimistic) => ProductList(products: value), 125 | orElse: () => EmptyView(), 126 | ); 127 | } 128 | ``` 129 | 130 | ```dart 131 | @override 132 | Widget build(BuildContext context) 133 | return state.products.mapError( 134 | error: (error, stackTrace) => ErrorView(error: error), 135 | orElse: () => ResulView(), 136 | ); 137 | } 138 | ``` 139 | 140 | ```dart 141 | @override 142 | Widget build(BuildContext context) 143 | return state.products.mapLoading( 144 | loading: (error, stackTrace) => LoadingView(), 145 | notLoading: () => ResulView(), 146 | ); 147 | } 148 | ``` 149 | 150 | 151 | ```dart 152 | @override 153 | Widget build(BuildContext context) 154 | return state.products.map( 155 | updated: (state) => UpdatedView(), 156 | failedRefresh: (state) => FailedView(), 157 | refreshing: (state) => LoadingView(), 158 | updating: (state) => LoadingView(), 159 | failedUpdate: (state) => FailedView(), 160 | notLoaded: (state) => NotLoadedView(), 161 | ); 162 | } 163 | ``` 164 | 165 | #### UpdateNotifier 166 | 167 | An `UpdateNotifier` is a mutable store for an `Update` instance. 168 | 169 | The [update] is updated by calling [execute]. 170 | 171 | ```dart 172 | final notifier = UpdateNotifier(); 173 | 174 | final currentUpdate = notifier.update; 175 | 176 | notifier.updateChanged.listen((update) { 177 | // ... 178 | }); 179 | 180 | notifier.execute( 181 | // The actual async operation 182 | updater: () async { 183 | // your async calls 184 | }, 185 | // This value will be available durint loading states 186 | optimisticValue: 32, 187 | // To change the behaviour when an update is triggered but the previous isn't finished yet. 188 | // If a previous update is cancelled then its stream is closed an no more events are emitted. 189 | override: UpdateOverride.cancelPrevious, 190 | )); 191 | ``` 192 | 193 | > If you're using patterns base on immutable data (like redux, MVU), using `update` function directly may be more appropriate. -------------------------------------------------------------------------------- /lib/src/future.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'update.dart'; 4 | 5 | /// Describes the behaviour of an update when one is already running or expired. 6 | abstract class UpdateOverride { 7 | const UpdateOverride._(); 8 | 9 | /// If a new update is requested and there already an ongoing update, the new update is ignored 10 | /// and the previous one continues. 11 | static const UpdateOverride ignore = _UpdateOverrideIgnore(); 12 | 13 | /// If a new update is requested and there already an ongoing update, the previous one is cancelled 14 | /// and the new update starts. 15 | static const UpdateOverride cancelPrevious = _UpdateOverrideCancelPrevious(); 16 | } 17 | 18 | /// Starts a sequence of updates by running the [updater] and raising a new [Update] after each step of its execution. 19 | /// 20 | /// At each step of the updater execution, the [getUpdate] will be used to check if the task has been cancelled. 21 | /// If so, the resulting stream will end without any new update. 22 | /// 23 | /// If an update has already been started from the item returned by [getUpdate], then the behaviour is controlled 24 | /// by the [override] parameters. The new update can whether be ignore, or cancel the previous execution. 25 | /// 26 | /// An [optimisticValue] can be given to display an anticipated result during the loading phase. 27 | /// 28 | /// If [expirationDate] is given, and the update has already been updated, then a new update is triggered only if the 29 | /// the given date is before [DateTime.now]. 30 | /// 31 | /// If [expirationDuration] is given, and no [expirationDate] is given, then [expirationDate] becomes the last updated 32 | /// date with the duration added to it. 33 | Stream> update({ 34 | @required Future Function() updater, 35 | @required Update Function() getUpdate, 36 | UpdateOverride override = UpdateOverride.ignore, 37 | T optimisticValue, 38 | DateTime expirationDate, 39 | Duration expirationDuration, 40 | }) { 41 | assert(getUpdate != null); 42 | assert(updater != null); 43 | return getUpdate().map( 44 | notLoaded: (state) async* { 45 | final updating = Updating.fromNotLoaded( 46 | state, 47 | id: _createId(), 48 | optimisticValue: optimisticValue, 49 | ); 50 | yield updating; 51 | try { 52 | final result = await updater(); 53 | if (!getUpdate().isCancelled(updating.id)) { 54 | yield Updated.fromUpdating(updating, result); 55 | } 56 | } catch (error, stackTrace) { 57 | if (!getUpdate().isCancelled(updating.id)) { 58 | yield FailedUpdate.fromUpdating( 59 | updating, 60 | error: error, 61 | stackTrace: stackTrace, 62 | ); 63 | } 64 | } 65 | }, 66 | updating: (state) async* { 67 | if (override is _UpdateOverrideCancelPrevious) { 68 | final updating = Updating.cancelling( 69 | state, 70 | id: _createId(), 71 | optimisticValue: optimisticValue, 72 | ); 73 | yield updating; 74 | try { 75 | final result = await updater(); 76 | if (!getUpdate().isCancelled(updating.id)) { 77 | yield Updated.fromUpdating(updating, result); 78 | } 79 | } catch (error, stackTrace) { 80 | if (!getUpdate().isCancelled(updating.id)) { 81 | yield FailedUpdate.fromUpdating( 82 | updating, 83 | error: error, 84 | stackTrace: stackTrace, 85 | ); 86 | } 87 | } 88 | } 89 | }, 90 | updated: (state) async* { 91 | if (expirationDuration != null) { 92 | expirationDate ??= state.updatedAt.add(expirationDuration); 93 | } 94 | final isExpired = 95 | expirationDate == null || expirationDate.isBefore(DateTime.now()); 96 | if (isExpired) { 97 | final updating = Refreshing.fromUpdated( 98 | state, 99 | id: _createId(), 100 | optimisticValue: optimisticValue, 101 | ); 102 | yield updating; 103 | try { 104 | final result = await updater(); 105 | if (!getUpdate().isCancelled(updating.id)) { 106 | yield Updated.fromRefreshing(updating, result); 107 | } 108 | } catch (error, stackTrace) { 109 | if (!getUpdate().isCancelled(updating.id)) { 110 | yield FailedRefresh.fromRefreshing( 111 | updating, 112 | error: error, 113 | stackTrace: stackTrace, 114 | ); 115 | } 116 | } 117 | } 118 | }, 119 | failedRefresh: (state) async* { 120 | final updating = Refreshing.fromFailed( 121 | state, 122 | id: _createId(), 123 | optimisticValue: optimisticValue, 124 | ); 125 | yield updating; 126 | try { 127 | final result = await updater(); 128 | if (!getUpdate().isCancelled(updating.id)) { 129 | yield Updated.fromRefreshing(updating, result); 130 | } 131 | } catch (error, stackTrace) { 132 | if (!getUpdate().isCancelled(updating.id)) { 133 | yield FailedRefresh.fromRefreshing( 134 | updating, 135 | error: error, 136 | stackTrace: stackTrace, 137 | ); 138 | } 139 | } 140 | }, 141 | failedUpdate: (state) async* { 142 | final updating = Updating.fromFailed( 143 | state, 144 | id: _createId(), 145 | optimisticValue: optimisticValue, 146 | ); 147 | yield updating; 148 | try { 149 | final result = await updater(); 150 | if (!getUpdate().isCancelled(updating.id)) { 151 | yield Updated.fromUpdating(updating, result); 152 | } 153 | } catch (error, stackTrace) { 154 | if (!getUpdate().isCancelled(updating.id)) { 155 | yield FailedUpdate.fromUpdating( 156 | updating, 157 | error: error, 158 | stackTrace: stackTrace, 159 | ); 160 | } 161 | } 162 | }, 163 | refreshing: (state) async* { 164 | if (override is _UpdateOverrideCancelPrevious) { 165 | final updating = Refreshing.cancelling( 166 | state, 167 | id: _createId(), 168 | optimisticValue: optimisticValue, 169 | ); 170 | yield updating; 171 | try { 172 | final result = await updater(); 173 | if (!getUpdate().isCancelled(updating.id)) { 174 | yield Updated.fromRefreshing(updating, result); 175 | } 176 | } catch (error, stackTrace) { 177 | if (!getUpdate().isCancelled(updating.id)) { 178 | yield FailedRefresh.fromRefreshing( 179 | updating, 180 | error: error, 181 | stackTrace: stackTrace, 182 | ); 183 | } 184 | } 185 | } 186 | }, 187 | ); 188 | } 189 | 190 | extension _UpdateExtensions on Update { 191 | /// Test if the [id] is the same as the update one. 192 | bool isCancelled(int id) { 193 | return map( 194 | failedRefresh: (state) => state.id != id, 195 | failedUpdate: (state) => state.id != id, 196 | updated: (state) => state.id != id, 197 | refreshing: (state) => state.id != id, 198 | updating: (state) => state.id != id, 199 | notLoaded: (state) => false, 200 | ); 201 | } 202 | } 203 | 204 | /// Create a new unique identifiers which will be associated to a new update. 205 | int _createId() => _lastId++; 206 | 207 | /// The unique identifiers associated to updates. 208 | int _lastId = DateTime.now().millisecondsSinceEpoch - 209 | DateTime(2020, 1, 1).millisecondsSinceEpoch; 210 | 211 | class _UpdateOverrideIgnore extends UpdateOverride { 212 | const _UpdateOverrideIgnore() : super._(); 213 | } 214 | 215 | class _UpdateOverrideCancelPrevious extends UpdateOverride { 216 | const _UpdateOverrideCancelPrevious() : super._(); 217 | } 218 | -------------------------------------------------------------------------------- /test/future_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:updated/updated.dart'; 3 | import 'package:updated/updated.dart' as u; 4 | import 'package:test/test.dart'; 5 | 6 | class TestStore { 7 | Update value; 8 | TestStore.notUpdated() : value = Update(); 9 | 10 | TestStore.updating({ 11 | @required int id, 12 | }) : value = Updating.fromNotLoaded(Update(), id: id); 13 | 14 | TestStore.refreshing({ 15 | @required int id, 16 | @required T value, 17 | }) : value = Refreshing.fromUpdated( 18 | Updated.fromUpdating( 19 | Updating.fromNotLoaded(Update(), id: id), 20 | value, 21 | ), 22 | id: id); 23 | 24 | TestStore.updated({ 25 | @required int id, 26 | @required T value, 27 | }) : value = Updated.fromUpdating( 28 | Updating.fromNotLoaded(Update(), id: id), 29 | value, 30 | ); 31 | 32 | Future>> update({ 33 | @required Future Function() updater, 34 | UpdateOverride override = UpdateOverride.ignore, 35 | T optimisticValue, 36 | }) async { 37 | final result = >[]; 38 | await for (var item in u.update( 39 | updater: updater, 40 | getUpdate: () => value, 41 | override: override, 42 | optimisticValue: optimisticValue, 43 | )) { 44 | value = item; 45 | result.add(item); 46 | } 47 | return result; 48 | } 49 | } 50 | 51 | void main() { 52 | group('An update call', () { 53 | test('transitions from `NotLoaded` to `Updating` then `Updated`', () async { 54 | final store = TestStore.notUpdated(); 55 | 56 | final updates = await store.update( 57 | updater: () async { 58 | await Future.delayed(const Duration(seconds: 1)); 59 | return 42; 60 | }, 61 | ); 62 | 63 | expect(updates.length, 2); 64 | 65 | // Updating 66 | final updating = updates.first; 67 | expect(updating.isLoading, isTrue); 68 | expect(updating.hasValue, isFalse); 69 | expect( 70 | updating.map( 71 | updating: (_) => true, 72 | notLoaded: (_) => false, 73 | failedUpdate: (_) => false, 74 | refreshing: (_) => false, 75 | failedRefresh: (_) => false, 76 | updated: (_) => false, 77 | ), 78 | isTrue); 79 | 80 | // Updated 81 | final updated = updates[1]; 82 | expect(updated.isLoading, isFalse); 83 | expect(updated.hasValue, isTrue); 84 | expect( 85 | updated.map( 86 | updating: (_) => false, 87 | notLoaded: (_) => false, 88 | failedUpdate: (_) => false, 89 | refreshing: (_) => false, 90 | failedRefresh: (_) => false, 91 | updated: (_) => true, 92 | ), 93 | isTrue); 94 | expect(updated.getValue(defaultValue: () => throw Exception), equals(42)); 95 | }); 96 | 97 | test('transitions from `NotLoaded` to `Updating` with an optimistic value', 98 | () async { 99 | final store = TestStore.notUpdated(); 100 | 101 | final updates = await store.update( 102 | optimisticValue: 71, 103 | updater: () async { 104 | await Future.delayed(const Duration(seconds: 1)); 105 | return 42; 106 | }, 107 | ); 108 | 109 | expect(updates.length, 2); 110 | 111 | // Updating 112 | final updating = updates.first; 113 | expect(updating.isLoading, isTrue); 114 | expect(updating.hasValue, isTrue); 115 | expect( 116 | updating.map( 117 | updating: (_) => true, 118 | notLoaded: (_) => false, 119 | failedUpdate: (_) => false, 120 | refreshing: (_) => false, 121 | failedRefresh: (_) => false, 122 | updated: (_) => false, 123 | ), 124 | isTrue); 125 | expect( 126 | updating.getValue(defaultValue: () => throw Exception), equals(71)); 127 | 128 | // Updated 129 | final updated = updates[1]; 130 | expect(updated.isLoading, isFalse); 131 | expect(updated.hasValue, isTrue); 132 | expect( 133 | updated.map( 134 | updating: (_) => false, 135 | notLoaded: (_) => false, 136 | failedUpdate: (_) => false, 137 | refreshing: (_) => false, 138 | failedRefresh: (_) => false, 139 | updated: (_) => true, 140 | ), 141 | isTrue); 142 | expect(updated.getValue(defaultValue: () => throw Exception), equals(42)); 143 | }); 144 | 145 | test('transitions from `NotLoaded` to `Updating` then `FailedUpdate`', 146 | () async { 147 | final store = TestStore.notUpdated(); 148 | 149 | final updates = await store.update( 150 | updater: () async { 151 | await Future.delayed(const Duration(seconds: 1)); 152 | throw Exception('TEST'); 153 | }, 154 | ); 155 | 156 | expect(updates.length, 2); 157 | 158 | // Updating 159 | final updating = updates.first; 160 | expect(updating.isLoading, isTrue); 161 | expect(updating.hasValue, isFalse); 162 | expect( 163 | updating.map( 164 | updating: (_) => true, 165 | notLoaded: (_) => false, 166 | failedUpdate: (_) => false, 167 | refreshing: (_) => false, 168 | failedRefresh: (_) => false, 169 | updated: (_) => false, 170 | ), 171 | isTrue); 172 | 173 | // FailedUpdate 174 | final failed = updates[1]; 175 | expect(failed.hasFailed, isTrue); 176 | expect(failed.isLoading, isFalse); 177 | expect(failed.hasValue, isFalse); 178 | expect( 179 | failed.map( 180 | updating: (_) => false, 181 | notLoaded: (_) => false, 182 | failedUpdate: (_) => true, 183 | refreshing: (_) => false, 184 | failedRefresh: (_) => false, 185 | updated: (_) => false, 186 | ), 187 | isTrue); 188 | expect( 189 | failed.mapError( 190 | error: (error, stackTrace) => error.message, 191 | orElse: () => throw Exception, 192 | ), 193 | equals('TEST')); 194 | }); 195 | }); 196 | 197 | test('transitions from `Updated` to `Refreshing` then `Updated`', () async { 198 | final store = TestStore.updated(id: 1, value: 42); 199 | 200 | // Starting a refresh 201 | final updates = await store.update( 202 | updater: () async { 203 | await Future.delayed(const Duration(seconds: 1)); 204 | return 24; 205 | }, 206 | ); 207 | 208 | expect(updates.length, 2); 209 | 210 | // Refreshing 211 | final refreshing = updates.first; 212 | expect(refreshing.isLoading, isTrue); 213 | expect(refreshing.hasValue, isTrue); 214 | expect( 215 | refreshing.map( 216 | updating: (_) => false, 217 | notLoaded: (_) => false, 218 | failedUpdate: (_) => false, 219 | refreshing: (_) => true, 220 | failedRefresh: (_) => false, 221 | updated: (_) => false, 222 | ), 223 | isTrue); 224 | expect( 225 | refreshing.getValue(defaultValue: () => throw Exception), equals(42)); 226 | 227 | // Updated 228 | final refreshed = updates[1]; 229 | expect(refreshed.isLoading, isFalse); 230 | expect(refreshed.hasValue, isTrue); 231 | expect( 232 | refreshed.map( 233 | updating: (_) => false, 234 | notLoaded: (_) => false, 235 | failedUpdate: (_) => false, 236 | refreshing: (_) => false, 237 | failedRefresh: (_) => false, 238 | updated: (_) => true, 239 | ), 240 | isTrue); 241 | expect(refreshed.getValue(defaultValue: () => throw Exception), equals(24)); 242 | }); 243 | 244 | test('does nothing from `Updating` with ignore override', () async { 245 | final store = TestStore.updating(id: 1); 246 | 247 | // Starting a refresh 248 | final updates = await store.update( 249 | override: UpdateOverride.ignore, 250 | updater: () async { 251 | await Future.delayed(const Duration(seconds: 1)); 252 | return 24; 253 | }, 254 | ); 255 | 256 | expect(updates.length, 0); 257 | 258 | // Updating 259 | final updating = store.value; 260 | expect(updating.isLoading, isTrue); 261 | expect(updating.hasValue, isFalse); 262 | expect( 263 | updating.map( 264 | updating: (_) => true, 265 | notLoaded: (_) => false, 266 | failedUpdate: (_) => false, 267 | refreshing: (_) => false, 268 | failedRefresh: (_) => false, 269 | updated: (_) => false, 270 | ), 271 | isTrue); 272 | }); 273 | 274 | test('does nothing from `Refreshing` with ignore override', () async { 275 | final store = TestStore.refreshing(id: 1, value: 42); 276 | 277 | // Starting a refresh 278 | final updates = await store.update( 279 | override: UpdateOverride.ignore, 280 | updater: () async { 281 | await Future.delayed(const Duration(seconds: 1)); 282 | return 24; 283 | }, 284 | ); 285 | 286 | expect(updates.length, 0); 287 | 288 | // Refreshing 289 | final refreshing = store.value; 290 | expect(refreshing.isLoading, isTrue); 291 | expect(refreshing.hasValue, isTrue); 292 | expect( 293 | refreshing.map( 294 | updating: (_) => false, 295 | notLoaded: (_) => false, 296 | failedUpdate: (_) => false, 297 | refreshing: (_) => true, 298 | failedRefresh: (_) => false, 299 | updated: (_) => false, 300 | ), 301 | isTrue); 302 | expect( 303 | refreshing.getValue(defaultValue: () => throw Exception), equals(42)); 304 | }); 305 | 306 | test('cancels `Updating` when cancelPrevious override', () async { 307 | final store = TestStore.notUpdated(); 308 | 309 | final cancelledUpdatesFuture = store.update( 310 | updater: () async { 311 | await Future.delayed(const Duration(seconds: 3)); 312 | return 24; 313 | }, 314 | ); 315 | 316 | await Future.delayed(const Duration(seconds: 1)); 317 | 318 | final overridingUpdatesFuture = store.update( 319 | override: UpdateOverride.cancelPrevious, 320 | updater: () async { 321 | await Future.delayed(const Duration(seconds: 1)); 322 | return 42; 323 | }, 324 | ); 325 | 326 | final cancelledUpdates = await cancelledUpdatesFuture; 327 | final overridingUpdates = await overridingUpdatesFuture; 328 | 329 | expect(cancelledUpdates.length, 1); 330 | 331 | // Cancelled : Updating 332 | final cancelUpdating = cancelledUpdates.first; 333 | expect(cancelUpdating.isLoading, isTrue); 334 | expect(cancelUpdating.hasValue, isFalse); 335 | expect( 336 | cancelUpdating.map( 337 | updating: (_) => true, 338 | notLoaded: (_) => false, 339 | failedUpdate: (_) => false, 340 | refreshing: (_) => false, 341 | failedRefresh: (_) => false, 342 | updated: (_) => false, 343 | ), 344 | isTrue); 345 | 346 | expect(overridingUpdates.length, 2); 347 | 348 | // Updating 349 | final updating = overridingUpdates.first; 350 | expect(updating.isLoading, isTrue); 351 | expect(updating.hasValue, isFalse); 352 | expect( 353 | updating.map( 354 | updating: (_) => true, 355 | notLoaded: (_) => false, 356 | failedUpdate: (_) => false, 357 | refreshing: (_) => false, 358 | failedRefresh: (_) => false, 359 | updated: (_) => false, 360 | ), 361 | isTrue); 362 | 363 | // Updated 364 | final updated = overridingUpdates[1]; 365 | expect(updated.isLoading, isFalse); 366 | expect(updated.hasValue, isTrue); 367 | expect( 368 | updated.map( 369 | updating: (_) => false, 370 | notLoaded: (_) => false, 371 | failedUpdate: (_) => false, 372 | refreshing: (_) => false, 373 | failedRefresh: (_) => false, 374 | updated: (_) => true, 375 | ), 376 | isTrue); 377 | expect(updated.getValue(defaultValue: () => throw Exception), equals(42)); 378 | }); 379 | } 380 | -------------------------------------------------------------------------------- /lib/src/update.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:updated/updated.dart'; 3 | 4 | /// An update represents the lifecycle of a [T] value that can be loaded asynchronously. 5 | /// 6 | /// Its initial state is `NotLoaded`. During its first update, it goes through the `Updating` state 7 | /// then `Updated` or `FailedUpdate` states, whether it is a success or not. During the following updates, 8 | /// it goes through the `Refreshing` state then `Updated` or `FailedRefresh` states, whether it is a success or not. 9 | /// 10 | /// See also : 11 | /// 12 | /// * [update] method to update a value from a [Future] and manage transitions and cancellation. 13 | abstract class Update { 14 | /// Create a [NotLoaded] instance. 15 | const factory Update() = NotLoaded; 16 | 17 | /// Internal constructor. 18 | const Update._(); 19 | 20 | /// Map the update state to an [Update] where values are converted with [mapValue]. 21 | Update mapUpdate({ 22 | @required K Function(T value1) mapValue, 23 | }) { 24 | return map( 25 | notLoaded: (notLoaded) => NotLoaded(), 26 | updating: (updating) => Updating( 27 | id: updating.id, 28 | optimisticValue: updating.optimisticValue != null 29 | ? mapValue(updating.optimisticValue) 30 | : null, 31 | ), 32 | updated: (updated) => Updated( 33 | id: updated.id, 34 | updatedAt: updated.updatedAt, 35 | value: mapValue(updated.value), 36 | ), 37 | refreshing: (refreshing) => Refreshing.fromUpdated( 38 | Updated( 39 | id: refreshing.previousUpdate.id, 40 | updatedAt: refreshing.previousUpdate.updatedAt, 41 | value: mapValue(refreshing.previousUpdate.value), 42 | ), 43 | id: refreshing.previousUpdate.id, 44 | optimisticValue: refreshing.optimisticValue != null 45 | ? mapValue(refreshing.optimisticValue) 46 | : null, 47 | ), 48 | failedUpdate: (failedUpdate) => FailedUpdate( 49 | id: failedUpdate.id, 50 | error: failedUpdate.error, 51 | stackTrace: failedUpdate.stackTrace, 52 | ), 53 | failedRefresh: (failedRefresh) => FailedRefresh( 54 | previousUpdate: Updated( 55 | id: failedRefresh.previousUpdate.id, 56 | updatedAt: failedRefresh.previousUpdate.updatedAt, 57 | value: mapValue(failedRefresh.previousUpdate.value), 58 | ), 59 | error: failedRefresh.error, 60 | stackTrace: failedRefresh.stackTrace, 61 | ), 62 | ); 63 | } 64 | 65 | /// Combine [update1] and [update2] as a new [Update]. 66 | /// 67 | /// If two values are available, they are merged into a new one 68 | /// with [combineValue]. 69 | static Update combine({ 70 | @required Update update1, 71 | @required Update update2, 72 | @required T3 Function(T1 value1, T2 value2) combineValues, 73 | }) { 74 | assert(update1 != null); 75 | assert(update2 != null); 76 | assert(combineValues != null); 77 | 78 | return update1.map( 79 | notLoaded: (state1) => update2.maybeMap( 80 | failedUpdate: (state2) => FailedUpdate( 81 | id: state2.id ^ state2.id, 82 | error: state2.error, 83 | stackTrace: state2.error, 84 | ), 85 | refreshing: (state2) => NotLoaded(), 86 | failedRefresh: (state2) => FailedUpdate( 87 | id: state2.id ^ state2.id, 88 | error: state2.error, 89 | stackTrace: state2.error, 90 | ), 91 | orElse: () => NotLoaded(), 92 | ), 93 | updating: (state1) => update2.map( 94 | failedUpdate: (state2) => FailedUpdate( 95 | id: state1.id ^ state2.id, 96 | error: state2.error, 97 | stackTrace: state2.error, 98 | ), 99 | refreshing: (state2) => Updating( 100 | id: state2.id ^ state2.id, 101 | ), 102 | failedRefresh: (state2) => FailedUpdate( 103 | id: state1.id ^ state2.id, 104 | error: state2.error, 105 | stackTrace: state2.error, 106 | ), 107 | notLoaded: (state2) => NotLoaded(), 108 | updated: (state2) => Updating( 109 | id: state1.id ^ state2.id, 110 | ), 111 | updating: (state2) => Updating( 112 | id: state1.id ^ state2.id, 113 | ), 114 | ), 115 | failedUpdate: (state1) => FailedUpdate( 116 | id: state1.id, 117 | error: state1.error, 118 | stackTrace: state1.error, 119 | ), 120 | refreshing: (state1) => update2.map( 121 | failedUpdate: (state2) => FailedUpdate( 122 | id: state1.id ^ state2.id, 123 | error: state2.error, 124 | stackTrace: state2.error, 125 | ), 126 | refreshing: (state2) => Refreshing.fromUpdated( 127 | Updated( 128 | id: state1.id ^ state2.id, 129 | updatedAt: state1.previousUpdate.updatedAt, 130 | value: combineValues( 131 | state1.previousUpdate.value, 132 | state2.previousUpdate.value, 133 | ), 134 | ), 135 | id: state2.id ^ state2.id, 136 | ), 137 | failedRefresh: (state2) => FailedRefresh( 138 | previousUpdate: Updated( 139 | id: state1.id ^ state2.id, 140 | updatedAt: state1.previousUpdate.updatedAt, 141 | value: combineValues( 142 | state1.previousUpdate.value, 143 | state2.previousUpdate.value, 144 | ), 145 | ), 146 | error: state2.error, 147 | stackTrace: state2.error, 148 | ), 149 | notLoaded: (state2) => NotLoaded(), 150 | updated: (state2) => Refreshing.fromUpdated( 151 | Updated( 152 | id: state1.id ^ state2.id, 153 | updatedAt: state1.previousUpdate.updatedAt, 154 | value: combineValues( 155 | state1.previousUpdate.value, 156 | state2.value, 157 | ), 158 | ), 159 | id: state2.id ^ state2.id, 160 | ), 161 | updating: (state2) => Updating( 162 | id: state1.id ^ state2.id, 163 | ), 164 | ), 165 | failedRefresh: (state1) => update2.map( 166 | failedUpdate: (state2) => FailedUpdate( 167 | id: state1.id ^ state2.id, 168 | error: state2.error, 169 | stackTrace: state2.error, 170 | ), 171 | refreshing: (state2) => FailedRefresh( 172 | previousUpdate: Updated( 173 | id: state1.id ^ state2.id, 174 | updatedAt: state1.previousUpdate.updatedAt, 175 | value: combineValues( 176 | state1.previousUpdate.value, 177 | state2.previousUpdate.value, 178 | ), 179 | ), 180 | error: state1.error, 181 | stackTrace: state1.error, 182 | ), 183 | failedRefresh: (state2) => FailedRefresh( 184 | previousUpdate: Updated( 185 | id: state1.id ^ state2.id, 186 | updatedAt: state1.previousUpdate.updatedAt, 187 | value: combineValues( 188 | state1.previousUpdate.value, 189 | state2.previousUpdate.value, 190 | ), 191 | ), 192 | error: state2.error, 193 | stackTrace: state2.error, 194 | ), 195 | notLoaded: (state2) => FailedUpdate( 196 | id: state1.id, 197 | error: state1.error, 198 | stackTrace: state1.stackTrace, 199 | ), 200 | updated: (state2) => FailedRefresh( 201 | previousUpdate: Updated( 202 | id: state1.id ^ state2.id, 203 | updatedAt: state1.previousUpdate.updatedAt, 204 | value: combineValues( 205 | state1.previousUpdate.value, 206 | state2.value, 207 | ), 208 | ), 209 | error: state1.error, 210 | stackTrace: state1.error, 211 | ), 212 | updating: (state2) => Updating( 213 | id: state1.id ^ state2.id, 214 | ), 215 | ), 216 | updated: (state1) => update2.map( 217 | failedUpdate: (state2) => FailedUpdate( 218 | id: state1.id ^ state2.id, 219 | error: state2.error, 220 | stackTrace: state2.error, 221 | ), 222 | refreshing: (state2) => Refreshing.fromUpdated( 223 | Updated( 224 | id: state1.id ^ state2.id, 225 | updatedAt: state1.updatedAt, 226 | value: combineValues( 227 | state1.value, 228 | state2.previousUpdate.value, 229 | ), 230 | ), 231 | id: state2.id ^ state2.id, 232 | ), 233 | failedRefresh: (state2) => FailedRefresh( 234 | previousUpdate: Updated( 235 | id: state1.id ^ state2.id, 236 | updatedAt: state1.updatedAt, 237 | value: combineValues( 238 | state1.value, 239 | state2.previousUpdate.value, 240 | ), 241 | ), 242 | error: state2.error, 243 | stackTrace: state2.error, 244 | ), 245 | notLoaded: (state2) => NotLoaded(), 246 | updated: (state2) => Updated( 247 | id: state1.id ^ state2.id, 248 | updatedAt: state1.updatedAt, 249 | value: combineValues( 250 | state1.value, 251 | state2.value, 252 | ), 253 | ), 254 | updating: (state2) => Updating( 255 | id: state1.id ^ state2.id, 256 | ), 257 | ), 258 | ); 259 | } 260 | 261 | /// Gets the value if available (regardless of it is an optimistic one or not), else returns the 262 | /// result of [defaultValue]. 263 | T getValue({ 264 | @required T Function() defaultValue, 265 | }) { 266 | assert(defaultValue != null); 267 | return mapValue( 268 | value: (value, _) => value, 269 | orElse: () => defaultValue(), 270 | ); 271 | } 272 | 273 | /// Indicates whether a value is currently available (regardless of it is an optimistic one or not). 274 | bool get hasValue { 275 | return mapValue( 276 | value: (value, _) => true, 277 | orElse: () => false, 278 | ); 279 | } 280 | 281 | /// Indicates whether the update is currently [Updating] or [Refreshing]. 282 | bool get isLoading { 283 | return map( 284 | updated: (state) => false, 285 | failedRefresh: (state) => false, 286 | refreshing: (state) => true, 287 | failedUpdate: (state) => false, 288 | notLoaded: (state) => false, 289 | updating: (state) => true, 290 | ); 291 | } 292 | 293 | /// Indicates whether the update is currently [FailedUpdate] or [FailedRefresh]. 294 | bool get hasFailed { 295 | return map( 296 | updated: (state) => false, 297 | failedRefresh: (state) => true, 298 | refreshing: (state) => false, 299 | failedUpdate: (state) => true, 300 | notLoaded: (state) => false, 301 | updating: (state) => false, 302 | ); 303 | } 304 | 305 | /// Indicates whether the update is currently [Updated]. 306 | bool get hasSucceeded { 307 | return map( 308 | updated: (state) => true, 309 | failedRefresh: (state) => false, 310 | refreshing: (state) => false, 311 | failedUpdate: (state) => false, 312 | notLoaded: (state) => false, 313 | updating: (state) => false, 314 | ); 315 | } 316 | 317 | /// Map the current value to a [K] value , if available. If so, the [value] method is called, else 318 | /// the [orElse] method is called. 319 | K mapValue({ 320 | @required K Function(T value, bool isOptimistic) value, 321 | @required K Function() orElse, 322 | }) { 323 | assert(value != null); 324 | assert(orElse != null); 325 | return map( 326 | updated: (state) => value(state.value, false), 327 | failedRefresh: (state) => value(state.previousUpdate.value, false), 328 | refreshing: (state) => state.optimisticValue != null 329 | ? value(state.optimisticValue, true) 330 | : value(state.previousUpdate.value, false), 331 | failedUpdate: (state) => orElse(), 332 | notLoaded: (state) => orElse(), 333 | updating: (state) => state.optimisticValue != null 334 | ? value(state.optimisticValue, true) 335 | : orElse(), 336 | ); 337 | } 338 | 339 | /// Map the current loading state to a [K] value. If [Updating] or [refreshing], the [loading] method is 340 | /// called, else the [orElse] method is called. 341 | K mapLoading({ 342 | @required K Function() loading, 343 | @required K Function() notLoading, 344 | }) { 345 | return map( 346 | updating: (state) => loading(), 347 | refreshing: (state) => loading(), 348 | updated: (state) => notLoading(), 349 | failedRefresh: (state) => notLoading(), 350 | failedUpdate: (state) => notLoading(), 351 | notLoaded: (state) => notLoading(), 352 | ); 353 | } 354 | 355 | /// Map the current error to a [K] value , if in a failure state. If so, the [error] method is called, else 356 | /// the [orElse] method is called. 357 | K mapError({ 358 | @required K Function(dynamic error, StackTrace stackTrace) error, 359 | @required K Function() orElse, 360 | }) { 361 | assert(error != null); 362 | assert(orElse != null); 363 | return map( 364 | failedRefresh: (state) => error(state.error, state.stackTrace), 365 | failedUpdate: (state) => error(state.error, state.stackTrace), 366 | updated: (state) => orElse(), 367 | refreshing: (state) => orElse(), 368 | notLoaded: (state) => orElse(), 369 | updating: (state) => orElse(), 370 | ); 371 | } 372 | 373 | /// Map the current state to a [K] value. 374 | K map({ 375 | @required K Function(NotLoaded state) notLoaded, 376 | @required K Function(Updating state) updating, 377 | @required K Function(FailedUpdate state) failedUpdate, 378 | @required K Function(Refreshing state) refreshing, 379 | @required K Function(FailedRefresh state) failedRefresh, 380 | @required K Function(Updated state) updated, 381 | }) { 382 | final state = this; 383 | if (state is Updated) { 384 | return updated(state); 385 | } 386 | if (state is NotLoaded) { 387 | return notLoaded(state); 388 | } 389 | if (state is FailedUpdate) { 390 | return failedUpdate(state); 391 | } 392 | if (state is Updating) { 393 | return updating(state); 394 | } 395 | if (state is Refreshing) { 396 | return refreshing(state); 397 | } 398 | if (state is FailedRefresh) { 399 | return failedRefresh(state); 400 | } 401 | 402 | throw Exception(); 403 | } 404 | 405 | /// Map the current state to a [K] value. 406 | /// 407 | /// The [orElse] callback is used for any missing case. 408 | K maybeMap({ 409 | @required K Function() orElse, 410 | K Function(NotLoaded state) notLoaded, 411 | K Function(Updating state) updating, 412 | K Function(FailedUpdate state) failedUpdate, 413 | K Function(Refreshing state) refreshing, 414 | K Function(FailedRefresh state) failedRefresh, 415 | K Function(Updated state) updated, 416 | }) { 417 | assert(orElse != null); 418 | final state = this; 419 | if (state is Updated) { 420 | return updated != null ? updated(state) : orElse(); 421 | } 422 | if (state is NotLoaded) { 423 | return notLoaded != null ? notLoaded(state) : orElse(); 424 | } 425 | if (state is FailedUpdate) { 426 | return failedUpdate != null ? failedUpdate(state) : orElse(); 427 | } 428 | if (state is Updating) { 429 | return updating != null ? updating(state) : orElse(); 430 | } 431 | if (state is Refreshing) { 432 | return refreshing != null ? refreshing(state) : orElse(); 433 | } 434 | if (state is FailedRefresh) { 435 | return failedRefresh != null ? failedRefresh(state) : orElse(); 436 | } 437 | 438 | return orElse(); 439 | } 440 | } 441 | 442 | class NotLoaded extends Update { 443 | const NotLoaded() : super._(); 444 | 445 | @override 446 | String toString() { 447 | return 'NotLoaded<$T>()'; 448 | } 449 | 450 | @override 451 | bool operator ==(dynamic other) { 452 | return identical(this, other) || other is NotLoaded; 453 | } 454 | 455 | @override 456 | int get hashCode => runtimeType.hashCode; 457 | } 458 | 459 | class Updating extends Update { 460 | Updating({ 461 | @required this.id, 462 | this.optimisticValue, 463 | }) : startedAt = DateTime.now(), 464 | super._(); 465 | 466 | Updating.fromNotLoaded( 467 | NotLoaded previous, { 468 | @required this.id, 469 | this.optimisticValue, 470 | }) : startedAt = DateTime.now(), 471 | super._(); 472 | 473 | Updating.fromFailed( 474 | FailedUpdate previous, { 475 | @required this.id, 476 | this.optimisticValue, 477 | }) : startedAt = DateTime.now(), 478 | super._(); 479 | 480 | Updating.cancelling( 481 | Updating previous, { 482 | @required this.id, 483 | this.optimisticValue, 484 | }) : startedAt = DateTime.now(), 485 | super._(); 486 | 487 | final int id; 488 | final T optimisticValue; 489 | final DateTime startedAt; 490 | 491 | @override 492 | String toString() { 493 | return 'Updating<$T>(id: $id, startedAt: $startedAt, optimisticValue: ${optimisticValue ?? 'none'})'; 494 | } 495 | 496 | @override 497 | bool operator ==(dynamic other) { 498 | return identical(this, other) || 499 | (other is Updating && 500 | id == other.id && 501 | optimisticValue == other.optimisticValue); 502 | } 503 | 504 | @override 505 | int get hashCode => 506 | runtimeType.hashCode ^ id.hashCode ^ (optimisticValue?.hashCode ?? 0); 507 | } 508 | 509 | class Updated extends Update { 510 | Updated.fromUpdating(Updating previous, this.value) 511 | : id = previous.id, 512 | updatedAt = DateTime.now(), 513 | super._(); 514 | 515 | Updated.fromRefreshing(Refreshing previous, this.value) 516 | : id = previous.id, 517 | updatedAt = DateTime.now(), 518 | super._(); 519 | 520 | const Updated({ 521 | @required this.id, 522 | @required this.updatedAt, 523 | @required this.value, 524 | }) : super._(); 525 | 526 | final int id; 527 | final DateTime updatedAt; 528 | final T value; 529 | 530 | @override 531 | String toString() { 532 | return 'Updated<$T>(id: $id, updatedAt: $updatedAt, previousUpdate: $value)'; 533 | } 534 | 535 | @override 536 | bool operator ==(dynamic other) { 537 | return identical(this, other) || 538 | (other is Updated && id == other.id && value == other.value); 539 | } 540 | 541 | @override 542 | int get hashCode => 543 | runtimeType.hashCode ^ id.hashCode ^ (value?.hashCode ?? 0); 544 | } 545 | 546 | class FailedUpdate extends Update { 547 | FailedUpdate.fromUpdating( 548 | Updating previous, { 549 | @required this.error, 550 | @required this.stackTrace, 551 | }) : failedAt = DateTime.now(), 552 | id = previous.id, 553 | super._(); 554 | 555 | FailedUpdate({ 556 | @required this.id, 557 | @required this.error, 558 | @required this.stackTrace, 559 | }) : failedAt = DateTime.now(), 560 | super._(); 561 | 562 | final int id; 563 | final DateTime failedAt; 564 | final dynamic error; 565 | final StackTrace stackTrace; 566 | 567 | @override 568 | String toString() { 569 | return 'FailedUpdate<$T>(id: $id, error: $error, stackTrace: $stackTrace, failedAt: $failedAt)'; 570 | } 571 | 572 | @override 573 | bool operator ==(dynamic other) { 574 | return identical(this, other) || 575 | (other is FailedUpdate && id == other.id); 576 | } 577 | 578 | @override 579 | int get hashCode => runtimeType.hashCode ^ id.hashCode; 580 | } 581 | 582 | class Refreshing extends Update { 583 | Refreshing.fromUpdated( 584 | Updated previous, { 585 | @required this.id, 586 | this.optimisticValue, 587 | }) : startedAt = DateTime.now(), 588 | previousUpdate = previous, 589 | super._(); 590 | 591 | Refreshing.fromFailed( 592 | FailedRefresh previous, { 593 | @required this.id, 594 | this.optimisticValue, 595 | }) : startedAt = DateTime.now(), 596 | previousUpdate = previous.previousUpdate, 597 | super._(); 598 | 599 | Refreshing.cancelling( 600 | Refreshing previous, { 601 | @required this.id, 602 | this.optimisticValue, 603 | }) : startedAt = DateTime.now(), 604 | previousUpdate = previous.previousUpdate, 605 | super._(); 606 | 607 | final int id; 608 | final DateTime startedAt; 609 | final T optimisticValue; 610 | final Updated previousUpdate; 611 | 612 | @override 613 | String toString() { 614 | return 'Refreshing<$T>(id: $id, startedAt: $startedAt, previousUpdate: $previousUpdate, optimisticValue: ${optimisticValue ?? 'none'})'; 615 | } 616 | 617 | @override 618 | bool operator ==(dynamic other) { 619 | return identical(this, other) || 620 | (other is Refreshing && 621 | id == other.id && 622 | optimisticValue == other.optimisticValue && 623 | previousUpdate == other.previousUpdate); 624 | } 625 | 626 | @override 627 | int get hashCode => 628 | runtimeType.hashCode ^ 629 | id.hashCode ^ 630 | (optimisticValue?.hashCode ?? 0) ^ 631 | previousUpdate.hashCode; 632 | } 633 | 634 | class FailedRefresh extends Update { 635 | FailedRefresh.fromRefreshing( 636 | Refreshing previous, { 637 | @required this.error, 638 | @required this.stackTrace, 639 | }) : failedAt = DateTime.now(), 640 | id = previous.id, 641 | previousUpdate = previous.previousUpdate, 642 | super._(); 643 | 644 | FailedRefresh({ 645 | @required this.previousUpdate, 646 | @required this.error, 647 | @required this.stackTrace, 648 | }) : failedAt = DateTime.now(), 649 | id = previousUpdate.id, 650 | super._(); 651 | 652 | final int id; 653 | final Updated previousUpdate; 654 | final DateTime failedAt; 655 | final dynamic error; 656 | final StackTrace stackTrace; 657 | 658 | @override 659 | String toString() { 660 | return 'FailedRefresh<$T>(id: $id, error: $error, stackTrace: $stackTrace, failedAt: $failedAt, previousUpdate: $previousUpdate)'; 661 | } 662 | 663 | @override 664 | bool operator ==(dynamic other) { 665 | return identical(this, other) || 666 | (other is FailedRefresh && 667 | id == other.id && 668 | previousUpdate == other.previousUpdate); 669 | } 670 | 671 | @override 672 | int get hashCode => 673 | runtimeType.hashCode ^ id.hashCode ^ previousUpdate.hashCode; 674 | } 675 | --------------------------------------------------------------------------------