├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ └── cached_value.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── example.dart └── pubspec.yaml ├── lib ├── cached_value.dart └── src │ ├── cached_value.dart │ ├── dependent_cached_value.dart │ ├── simple_cached_value.dart │ ├── single_child_cached_value.dart │ └── time_to_live_cached_value.dart ├── pubspec.yaml └── test └── src ├── cached_value_test.dart ├── dependent_cached_value_test.dart ├── simple_cached_value_test.dart └── time_to_live_cached_value_test.dart /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Are you facing a problem using this package? Use this template to make it easier to reach into a solution. 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | 16 | **Describe the bug** 17 | 18 | 19 | **To Reproduce** 20 | 21 | 22 | **What is the current behavior?** 23 | 26 | 27 | **Expected behavior** 28 | 29 | 30 | **Screenshots** 31 | 32 | 33 | **Which versions of the package are affected by this issue? Did this work in previous versions of This package?** 34 | 35 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | groups: 8 | github-actions: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: pub 12 | directory: /example 13 | schedule: 14 | interval: monthly 15 | groups: 16 | example-pub: 17 | patterns: 18 | - "*" 19 | - package-ecosystem: pub 20 | directory: / 21 | schedule: 22 | interval: monthly 23 | groups: 24 | root-pub: 25 | patterns: 26 | - "*" 27 | -------------------------------------------------------------------------------- /.github/workflows/cached_value.yml: -------------------------------------------------------------------------------- 1 | name: cached_value 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | pull_request: 9 | paths: 10 | - ".github/workflows/cached_value.yaml" 11 | - "lib/**" 12 | - "test/**" 13 | - "pubspec.yaml" 14 | push: 15 | branches: 16 | - main 17 | paths: 18 | - ".github/workflows/cached_value.yaml" 19 | - "lib/**" 20 | - "test/**" 21 | - "pubspec.yaml" 22 | 23 | jobs: 24 | semantic-pull-request: 25 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 26 | 27 | build: 28 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1 29 | with: 30 | coverage_excludes: '**/*.g.dart' 31 | dart_sdk: 'stable' 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/ephemeral 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/Flutter/flutter_export_environment.sh 68 | **/ios/ServiceDefinitions.json 69 | **/ios/Runner/GeneratedPluginRegistrant.* 70 | 71 | # Exceptions to above rules. 72 | !**/ios/**/default.mode1v3 73 | !**/ios/**/default.mode2v3 74 | !**/ios/**/default.pbxuser 75 | !**/ios/**/default.perspectivev3 76 | 77 | pubspec.lock -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 98a96189937105f51e58e65543e2c6019a82cb33 8 | channel: master 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | * Update dependencies, lints and tests 4 | * Fix usage with sync generators 5 | * Update README 6 | 7 | 8 | ## 0.2.0 9 | 10 | * Add declarative API (#2) along with TTL. 11 | 12 | ## 0.1.0 13 | 14 | * Initial version 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Renan Araújo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cached_value 2 | 3 | [![Pub](https://img.shields.io/pub/v/cached_value.svg?style=popout)](https://pub.dartlang.org/packages/cached_value) 4 | 5 | A simple way to cache values that result from rather expensive operations. 6 | 7 | It is useful to cache values that: 8 | - Are computed from other values in a consistent way; 9 | - Can be changed given known and unknown conditions; 10 | - Should not be computed on every access (like a getter); 11 | 12 | A `cached_value` is better used over imperative APIs, such as Flutter's render objects. See [Motivation](#motivation) for more. 13 | 14 | ## Installation 15 | 16 | Add to pubspec.yaml: 17 | ```yaml 18 | dart pub add cached_value 19 | ``` 20 | 21 | ## Usage 22 | 23 | A cache can start as a simple manually controlled cache and then be enhanced with automatic functionalities such as dependencies and time-to-live (TTL). 24 | 25 | ### 1.Creating a simple cached that is invaldiated manually 26 | 27 | 28 | 29 | ```dart 30 | int factorial(int n) { 31 | if (n < 0) throw ('Negative numbers are not allowed.'); 32 | return n <= 1 ? 1 : n * factorial(n - 1); 33 | } 34 | 35 | int originalValue = 1; 36 | final factorialCache = CachedValue(() => factorial(originalValue)); 37 | print(factorialCache.value); // 1 38 | 39 | originalValue = 6; 40 | 41 | print(factorialCache.value); // 1 42 | print(factorialCache.isValid) // true, invalid only when invalidate is called 43 | 44 | // mark as invalid 45 | factorialCache.invalidate(); 46 | print(factorialCache.isValid); // false 47 | 48 | print(factorialCache.value); // 720 49 | print(factorialCache.isValid); // true 50 | ``` 51 | Accessing `value` when the cache is invalid refreshes the cache. It can be refreshed manually via 52 | the `refresh` method: 53 | 54 | ```dart 55 | // ... 56 | originalValue = 12; 57 | factorialCache.refresh(); 58 | 59 | print(factorialCache.value); // 12 60 | ``` 61 | 62 | ### 2. Adding dependencies 63 | 64 | A dependent cache is marked as invalid if its dependency value has changed. 65 | 66 | ```dart 67 | int factorial(int n) { 68 | if (n < 0) throw ('Negative numbers are not allowed.'); 69 | return n <= 1 ? 1 : n * factorial(n - 1); 70 | } 71 | 72 | int originalValue = 1; 73 | final factorialCache = CachedValue( 74 | () => factorial(originalValue), 75 | ).withDependency(() => originalValue); 76 | print(factorialCache.value); // 1 77 | print(factorialCache.isValid); // true 78 | 79 | // update value 80 | originalValue = 6; 81 | print(factorialCache.isValid); // false 82 | 83 | print(factorialCache.value); // 720 84 | print(factorialCache.isValid); // true 85 | ``` 86 | 87 | ⚠️Important: 88 | The dependency callback is called on every value access. So it is recommended to keep it as declarative as possible. 89 | 90 | ```dart 91 | final someCache = CachedValue( 92 | // ... 93 | ).withDependency( 94 | () => someExpensiveOperation(originalValue), // ❌ Avoid this: 95 | ); 96 | ``` 97 | 98 | 99 | 100 | ### 3. Adding time to live (TTL) 101 | 102 | A cache can be automatically marked as invalid some time after a refresh. 103 | 104 | ```dart 105 | int factorial(int n) { 106 | if (n < 0) throw ('Negative numbers are not allowed.'); 107 | return n <= 1 ? 1 : n * factorial(n - 1); 108 | } 109 | 110 | int originalValue = 1; 111 | final factorialCache = CachedValue( 112 | () => factorial(originalValue), 113 | ).withTimeToLive( 114 | lifetime: Duration(seconds: 3), 115 | ); 116 | 117 | originalValue = 6; 118 | 119 | print(factorialCache.value); // 1 120 | 121 | await Future.delayed(Duration(seconds: 3)); 122 | 123 | print(factorialCache.value); // 720 124 | ``` 125 | 126 | ## More docs 127 | 128 | There is more detailed docs on the [API documentation](https://pub.dev/documentation/cached_value/latest/). 129 | 130 | ## Motivation 131 | 132 | Some imperative APIs such as the canvas paint on Flutter render objects of Flame's components may 133 | benefit from values that can be stored and reused between more than a single frame (or paint). 134 | 135 | In some very specific cases, I found very convenient to store some objects across frames, like 136 | `Paint` and `TextPainter` instances. 137 | 138 | Example on a render object: 139 | ```dart 140 | class BlurredRenderObject extends RenderObject { 141 | 142 | // ... 143 | 144 | double _blurSigma = 0.0; 145 | double get blurSigma => _blurSigma; 146 | set blurSigma(double value) { 147 | _blurSigma = blurSigma; 148 | markNeedsPaint(); 149 | } 150 | 151 | // ... 152 | 153 | @override 154 | void paint(PaintingContext context, Offset offset) { 155 | final canvas = context.canvas; 156 | 157 | 158 | final paint = Paint()..maskFilter = MaskFilter.blur( 159 | BlurStyle.normal, blurSigma 160 | ); 161 | canvas.drawRect(Rect.fromLTWH(0, 0, 100, 100), paint); 162 | } 163 | } 164 | ``` 165 | 166 | Can be changed to: 167 | ```dart 168 | class BlurredRenderObject extends RenderObject { 169 | 170 | // ... 171 | 172 | double _blurSigma = 0.0; 173 | double get blurSigma => _blurSigma; 174 | set blurSigma(double value) { 175 | _blurSigma = blurSigma; 176 | markNeedsPaint(); 177 | } 178 | 179 | // Add cache: 180 | late final paintCache = CachedValue( 181 | () => Paint()..maskFilter = MaskFilter.blur(BlurStyle.normal, blurSigma), 182 | ).withDependency(() => blurSigma); 183 | 184 | // ... 185 | 186 | @override 187 | void paint(PaintingContext context, Offset offset) { 188 | final canvas = context.canvas; 189 | 190 | // use cache: 191 | final paint = paintCache.value; 192 | canvas.drawRect(Rect.fromLTWH(0, 0, 100, 100), paint); 193 | } 194 | } 195 | ``` -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.yaml 2 | linter: 3 | rules: 4 | public_member_api_docs: true 5 | one_member_abstracts: false 6 | 7 | 8 | analyzer: 9 | exclude: 10 | - "**/*.g.dart" 11 | - "lib/src/version.dart" -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'package:cached_value/cached_value.dart'; 4 | 5 | int factorial(int n) { 6 | if (n < 0) throw Exception('Negative numbers are not allowed.'); 7 | return n <= 1 ? 1 : n * factorial(n - 1); 8 | } 9 | 10 | void main() { 11 | withDependency(); 12 | withMultipleDependencies(); 13 | withTimeToLive(); 14 | } 15 | 16 | void withDependency() { 17 | print('with dependency'); 18 | 19 | var originalValue = 1; 20 | final factorialCache = CachedValue( 21 | () => factorial(originalValue), 22 | ).withDependency( 23 | () => originalValue, 24 | ); 25 | 26 | print(factorialCache.value); // 1 27 | 28 | print(factorialCache.value); // 1 - cached 29 | 30 | originalValue = 6; 31 | 32 | print(factorialCache.value); // 720 33 | } 34 | 35 | void withMultipleDependencies() { 36 | print('with multiple dependencies'); 37 | 38 | var originalValue1 = 1; 39 | var originalValue2 = 2; 40 | final factorialCache = CachedValue( 41 | () => factorial(originalValue1) + factorial(originalValue2), 42 | ).withDependency( 43 | () sync* { 44 | yield originalValue1; 45 | yield originalValue2; 46 | }, 47 | ); 48 | 49 | print(factorialCache.value); // 3 50 | 51 | print(factorialCache.value); // 3 - cached 52 | 53 | originalValue2 = 6; 54 | 55 | print(factorialCache.value); // 721 56 | } 57 | 58 | Future withTimeToLive() async { 59 | print('with TTL:'); 60 | 61 | var originalValue = 1; 62 | final factorialCache = CachedValue( 63 | () => factorial(originalValue), 64 | ).withTimeToLive( 65 | lifetime: const Duration(seconds: 3), 66 | ); 67 | 68 | originalValue = 6; 69 | 70 | print(factorialCache.value); // 1 71 | 72 | await Future.delayed(const Duration(seconds: 3)); 73 | 74 | print(factorialCache.value); // 720 75 | } 76 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: cached_value_example 2 | description: Example for cacjed value 3 | version: 0.0.1 4 | homepage: https://github.com/renancaraujo/cached_value 5 | publish_to: none 6 | 7 | environment: 8 | sdk: ">=2.12.0 <3.0.0" 9 | 10 | dependencies: 11 | cached_value: 12 | path: ../ 13 | -------------------------------------------------------------------------------- /lib/cached_value.dart: -------------------------------------------------------------------------------- 1 | /// A simple way to cache values that result from rather expensive operations. 2 | library cached_value; 3 | 4 | export 'src/cached_value.dart'; 5 | export 'src/dependent_cached_value.dart'; 6 | export 'src/simple_cached_value.dart'; 7 | export 'src/single_child_cached_value.dart'; 8 | export 'src/time_to_live_cached_value.dart'; 9 | -------------------------------------------------------------------------------- /lib/src/cached_value.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_value/src/dependent_cached_value.dart'; 2 | import 'package:cached_value/src/simple_cached_value.dart'; 3 | import 'package:cached_value/src/single_child_cached_value.dart'; 4 | import 'package:cached_value/src/time_to_live_cached_value.dart'; 5 | 6 | /// A signature for functions that computes the value to be cached. 7 | /// 8 | /// It is called: 9 | /// - On the creation of the cache; 10 | /// - When [CachedValue.refresh] is manually called; 11 | /// - When [CachedValue.value] is accessed and the cache is considered invalid. 12 | typedef ComputeCacheCallback = CacheContentType Function(); 13 | 14 | /// A value container that caches values resultant from a potentially expensive 15 | /// operation. 16 | /// 17 | /// It is convenient for storing values that: 18 | /// - Are computed from other values; 19 | /// - Can be changed given known and unknown conditions; 20 | /// - Should not be computed on every access; 21 | /// 22 | /// As an abstract class main constructor returns a [SimpleCachedValue] that 23 | /// creates a cache that can only be marked as invalid or refresh manually. 24 | /// 25 | /// To add automatic rules on invalidating and refreshing of a cache, see: 26 | /// - [DependentCachedValue] creates a cache that is updated if a 27 | /// dependency changes. 28 | /// - [TimeToLiveCachedValue] creates a cache that is invalidated after 29 | /// some given [Duration]. 30 | abstract class CachedValue { 31 | /// Creates a [CachedValue] that is only manually invalidated. 32 | /// 33 | /// The implementation type for the returned value is [SimpleCachedValue]. 34 | /// 35 | /// {@macro simple_cache} 36 | /// 37 | /// Usage example: 38 | /// ```dart 39 | /// int factorial(int n) { 40 | /// if (n < 0) throw ('Negative numbers are not allowed.'); 41 | /// return n <= 1 ? 1 : n * factorial(n - 1); 42 | /// } 43 | /// 44 | /// int originalValue =1; 45 | /// final factorialCache = CachedValue(() => factorial(originalValue)); 46 | /// print(factorialCache.value); // 1 47 | /// 48 | /// originalValue = 6; 49 | /// 50 | /// print(factorialCache.value); // 1 51 | /// factorialCache.invalidate(); 52 | /// 53 | /// print(factorialCache.value); // 720 54 | /// ``` 55 | /// 56 | /// See also: 57 | /// - [DependentCachedValue] creates a cache that is updated if a 58 | /// dependency changes. 59 | /// - [TimeToLiveCachedValue] creates a cache that is invalidated after 60 | /// some given [Duration]. 61 | factory CachedValue(ComputeCacheCallback callback) { 62 | return SimpleCachedValue(callback); 63 | } 64 | 65 | /// Access the current cache value. 66 | /// 67 | /// If the cache is considered invalid, calls [refresh]. 68 | CacheContentType get value; 69 | 70 | /// Check the current state of the cache. 71 | /// 72 | /// On a simple [SimpleCachedValue] caches it only checks 73 | /// if [invalidate] has been called. 74 | /// 75 | /// On a [DependentCachedValue] checks if the result of the dependency 76 | /// callback has changed and its child is valid. 77 | /// 78 | /// on [TimeToLiveCachedValue] checks if its ligetime has been spent and if 79 | /// its child is valid. 80 | bool get isValid; 81 | 82 | /// Marks the cache as invalid. 83 | /// 84 | /// This means that the cached value will be considered outdated and next time 85 | /// [value] is accessed, [refresh] will be called. 86 | /// 87 | /// Calling this on a subclass of [SingleChildCachedValue] 88 | /// makes the child also invalid. 89 | void invalidate(); 90 | 91 | /// Updates the cache to an updated state. 92 | /// 93 | /// It is called either manually or via the [value] getter when it is 94 | /// accessed and the cached is considered invalid. 95 | /// 96 | /// On [SimpleCachedValue], 97 | /// {@macro simple_refresh} 98 | /// 99 | /// On [DependentCachedValue], 100 | /// {@macro dependent_refresh} 101 | /// 102 | /// On [TimeToLiveCachedValue], 103 | /// {@macro ttl_refresh} 104 | /// 105 | /// {@template main_refresh} 106 | /// After refresh, the cache is considered valid. 107 | /// 108 | /// The returned value should be the new cache value. 109 | /// {@endtemplate} 110 | CacheContentType refresh(); 111 | } 112 | -------------------------------------------------------------------------------- /lib/src/dependent_cached_value.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_value/src/cached_value.dart'; 2 | import 'package:cached_value/src/single_child_cached_value.dart'; 3 | import 'package:collection/collection.dart'; 4 | 5 | /// A signature for functions that provides dependency of a 6 | /// [DependentCachedValue]. 7 | /// 8 | /// It s called in every [CachedValue.value] access. {@macro dependency_advice} 9 | typedef ComputeCacheDependency = DependencyType Function(); 10 | 11 | /// A [CachedValue] that its validity is defined by a dependency. 12 | /// It holds the last result of the computation callback since the last change 13 | /// on the result of the dependency callback. 14 | /// 15 | /// This cache will be considered invalid if the overall returned value of 16 | /// the dependency callback changes since the last refresh. 17 | /// 18 | /// Besides dependency change, this cache can also be manually updated on 19 | /// marked as invalid via [refresh] and [invalidate]. 20 | /// 21 | /// The dependency callback is called in every [value] access. 22 | /// {@template dependency_advice} 23 | /// So it is recommended to keep the dependency callback as declarative as 24 | /// possible. 25 | /// {@endtemplate} 26 | /// 27 | /// It can be created via `CachedValue.withDependency` 28 | class DependentCachedValue 29 | extends SingleChildCachedValue { 30 | DependentCachedValue._( 31 | CachedValue child, 32 | this._getDependency, 33 | ) : super(child); 34 | late DependencyType _dependencyCache = _getDependency(); 35 | 36 | @override 37 | CacheContentType get value { 38 | if (!isValid) { 39 | return refresh(); 40 | } 41 | return super.value; 42 | } 43 | 44 | @override 45 | bool get isValid => 46 | super.isValid && _equalityCompare(_dependencyCache, _getDependency()); 47 | 48 | final ComputeCacheDependency _getDependency; 49 | 50 | /// {@template dependent_refresh} 51 | /// Calls refresh on its child and updates the local cache of dependency. 52 | /// {@endtemplate} 53 | /// 54 | /// {@macro main_refresh} 55 | /// 56 | /// See also: 57 | /// - [CachedValue.refresh] for the general behavior of refresh. 58 | @override 59 | CacheContentType refresh() { 60 | final newValue = super.refresh(); 61 | _dependencyCache = _getDependency(); 62 | return newValue; 63 | } 64 | } 65 | 66 | const DeepCollectionEquality _collectionEquality = DeepCollectionEquality(); 67 | 68 | bool _equalityCompare(EqualityType a, EqualityType b) { 69 | if (a is Iterable || a is Map) { 70 | return _collectionEquality.equals(a, b); 71 | } 72 | return a == b; 73 | } 74 | 75 | /// Adds [withDependency] to [CachedValue]. 76 | extension DependentExtension 77 | on CachedValue { 78 | /// Wraps the declared [CachedValue] with a [DependentCachedValue]. 79 | /// 80 | /// The dependency callback [on] is called on every [value] access. 81 | /// {@macro dependency_advice} 82 | /// 83 | /// Usage example: 84 | /// ```dart 85 | /// int factorial(int n) { 86 | /// if (n < 0) throw ('Negative numbers are not allowed.'); 87 | /// return n <= 1 ? 1 : n * factorial(n - 1); 88 | /// } 89 | /// 90 | /// int originalValue = 1; 91 | /// final factorialCache = CachedValue(() => factorial(originalValue)) 92 | /// .withDependency( 93 | /// () => originalValue, 94 | /// ); 95 | /// print(factorialCache.value); // 1 96 | /// 97 | /// originalValue = 6; 98 | /// print(factorialCache.value); // 720 99 | /// ``` 100 | DependentCachedValue 101 | withDependency( 102 | ComputeCacheDependency on, 103 | ) { 104 | DependencyType getDependency() { 105 | final dependencyValue = on(); 106 | if (dependencyValue is Iterable && dependencyValue is! List) { 107 | return dependencyValue.toList() as DependencyType; 108 | } 109 | return dependencyValue; 110 | } 111 | 112 | return DependentCachedValue._(this, getDependency); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/src/simple_cached_value.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_value/src/cached_value.dart'; 2 | import 'package:cached_value/src/dependent_cached_value.dart'; 3 | import 'package:cached_value/src/single_child_cached_value.dart'; 4 | import 'package:cached_value/src/time_to_live_cached_value.dart'; 5 | 6 | /// A [CachedValue] that holds the last result of the computation callback since 7 | /// the start of the last time the cache was refresh via [refresh]. 8 | /// 9 | /// As a comparison to [TimeToLiveCachedValue] and [DependentCachedValue], 10 | /// it defines the most fundamental behavior of teh cache. 11 | /// 12 | /// {@template simple_cache} 13 | /// This cache type will only be considered invalid if [invalidate] is called 14 | /// manually (or by a [SingleChildCachedValue] that wraps a cache of this type). 15 | /// {@endtemplate} 16 | /// 17 | /// It is recommended to be created via [CachedValue]'s main constructor 18 | /// [new CachedValue]. 19 | class SimpleCachedValue 20 | implements CachedValue { 21 | /// Creates a [SimpleCachedValue]. It is recommended to use 22 | /// [new CachedValue] instead of this constructor. 23 | SimpleCachedValue(this._computeCache) { 24 | _value = _computeCache(); 25 | } 26 | late CacheContentType _value; 27 | 28 | bool _isValid = true; 29 | 30 | @override 31 | CacheContentType get value { 32 | if (!isValid) { 33 | return refresh(); 34 | } 35 | return _value; 36 | } 37 | 38 | @override 39 | bool get isValid => _isValid; 40 | 41 | final ComputeCacheCallback _computeCache; 42 | 43 | @override 44 | void invalidate() { 45 | _isValid = false; 46 | } 47 | 48 | /// {@template simple_refresh} 49 | /// Manually calls the compute update callback (given by the constructor) and 50 | /// store the result on cache. 51 | /// {@endtemplate} 52 | /// 53 | /// {@macro main_refresh} 54 | /// 55 | /// See also: 56 | /// - [CachedValue.refresh] for the general behavior of refresh. 57 | @override 58 | CacheContentType refresh() { 59 | _value = _computeCache(); 60 | _isValid = true; 61 | return _value; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/single_child_cached_value.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_value/cached_value.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | /// A type of [CachedValue] that contains another. 5 | /// 6 | /// All elements of its public interface calls its [child] methods. 7 | /// 8 | /// See also: 9 | /// - [DependentCachedValue] and [TimeToLiveCachedValue] that are the main 10 | /// implementations of this class. 11 | /// 12 | /// To subclass this, consider: 13 | /// - A cache type may define custom behaviors to when and how a cache should be 14 | /// invalidated and refreshed. 15 | /// - Avoid and verify for conflict with other cache types. 16 | abstract class SingleChildCachedValue 17 | implements CachedValue { 18 | /// Creates a cached value that wraps a [child] 19 | const SingleChildCachedValue(this.child); 20 | 21 | /// A [CachedValue] in which this cache wraps. 22 | final CachedValue child; 23 | 24 | @override 25 | @mustCallSuper 26 | void invalidate() => child.invalidate(); 27 | 28 | @override 29 | @mustCallSuper 30 | bool get isValid => child.isValid; 31 | 32 | @override 33 | @mustCallSuper 34 | CacheContentType refresh() => child.refresh(); 35 | 36 | @override 37 | @mustCallSuper 38 | CacheContentType get value => child.value; 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/time_to_live_cached_value.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:cached_value/src/cached_value.dart'; 4 | import 'package:cached_value/src/single_child_cached_value.dart'; 5 | 6 | /// A [CachedValue] that is invalidated some time after a refresh. 7 | /// 8 | /// It keeps an internal [Timer] that is restarted very time the cache is 9 | /// refreshed. 10 | /// 11 | /// After that time, [isValid] will be false and any access to [value] will 12 | /// trigger [refresh] and the timer will be restarted. 13 | /// 14 | /// Besides dependency change, this cache can also be manually updated on 15 | /// marked as invalid via [refresh] and [invalidate]. 16 | /// 17 | /// The duration of the timer is equal to [lifeTime]. 18 | /// 19 | /// Do not wrap a TTL cache on another, otherwise an assertion will be thrown. 20 | /// 21 | /// It can be created via `CachedValue.withTimeToLive` 22 | class TimeToLiveCachedValue 23 | extends SingleChildCachedValue { 24 | TimeToLiveCachedValue._(CachedValue child, this.lifeTime) 25 | : super(child) { 26 | assert(_debugVerifyDuplicity(), ''); 27 | _timer = Timer(lifeTime, () {}); 28 | } 29 | 30 | /// The amount of time that will take to the cache to be considered invalid 31 | /// after a refresh. 32 | final Duration lifeTime; 33 | late Timer _timer; 34 | 35 | @override 36 | bool get isValid => super.isValid && _timer.isActive; 37 | 38 | @override 39 | CacheContentType get value { 40 | if (!isValid) { 41 | return refresh(); 42 | } 43 | return super.value; 44 | } 45 | 46 | /// {@template ttl_refresh} 47 | /// Calls refresh on its [child] and restarts the internal [Timer]. 48 | /// {@endtemplate} 49 | /// 50 | /// {@macro main_refresh} 51 | /// 52 | /// See also: 53 | /// - [CachedValue.refresh] for the general behavior of refresh. 54 | @override 55 | CacheContentType refresh() { 56 | final newValue = super.refresh(); 57 | _startLifeAgain(); 58 | return newValue; 59 | } 60 | 61 | void _startLifeAgain() { 62 | if (_timer.isActive) { 63 | _timer.cancel(); 64 | } 65 | _timer = Timer(lifeTime, () {}); 66 | } 67 | 68 | bool _debugVerifyDuplicity() { 69 | assert(() { 70 | bool verifyDuplicity(CachedValue child) { 71 | if (child is TimeToLiveCachedValue) { 72 | return false; 73 | } 74 | if (child is SingleChildCachedValue) { 75 | return verifyDuplicity(child.child); 76 | } 77 | return true; 78 | } 79 | 80 | return verifyDuplicity(child); 81 | }(), ''' 82 | There is a declaration of a cached value time to live specified more than once'''); 83 | return true; 84 | } 85 | } 86 | 87 | /// Adds [withTimeToLive] to [CachedValue]. 88 | extension TimeToLiveExtension 89 | on CachedValue { 90 | /// Wraps the declared [CachedValue] with a [TimeToLiveCachedValue]. 91 | /// 92 | /// Usage example: 93 | /// ```dart 94 | /// int factorial(int n) { 95 | /// if (n < 0) throw ('Negative numbers are not allowed.'); 96 | /// return n <= 1 ? 1 : n * factorial(n - 1); 97 | /// } 98 | /// 99 | /// int originalValue = 1; 100 | /// final factorialCache = CachedValue( 101 | /// () => factorial(originalValue), 102 | /// ).withTimeToLive( 103 | /// lifetime: Duration(seconds: 3), 104 | /// ); 105 | /// 106 | /// originalValue = 6; 107 | /// print(factorialCache.value); // 1 108 | /// 109 | /// await Future.delayed(Duration(seconds: 3)); 110 | /// 111 | /// print(factorialCache.value); // 720 112 | /// ``` 113 | CachedValue withTimeToLive({ 114 | required Duration lifetime, 115 | }) => 116 | TimeToLiveCachedValue._(this, lifetime); 117 | } 118 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: cached_value 2 | description: A simple way to cache values that result from rather expensive operations. 3 | version: 1.0.0 4 | repository: https://github.com/bluefireteam/cached_value 5 | issue_tracker: https://github.com/bluefireteam/cached_value/issues 6 | homepage: https://github.com/bluefireteam/cached_value 7 | documentation: https://github.com/bluefireteam/cached_value?tab=readme-ov-file#cached_value 8 | topics: [cache, memoization, caching] 9 | 10 | environment: 11 | sdk: ">=2.12.0 <3.0.0" 12 | 13 | dependencies: 14 | collection: ^1.15.0 15 | meta: ^1.3.0 16 | 17 | dev_dependencies: 18 | test: ^1.25.8 19 | very_good_analysis: ^6.0.0 20 | -------------------------------------------------------------------------------- /test/src/cached_value_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_value/cached_value.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('Constructor creates a simple cached value', () { 6 | final cachedValue = CachedValue(() => 2); 7 | expect(cachedValue, const TypeMatcher>()); 8 | }); 9 | 10 | test('with dependency', () { 11 | const dependency = 2; 12 | final cachedValue = 13 | CachedValue(() => 4 / dependency).withDependency(() => dependency); 14 | expect( 15 | cachedValue, 16 | const TypeMatcher>(), 17 | ); 18 | }); 19 | 20 | test('with ttl', () { 21 | final cachedValue = CachedValue(() => 2).withTimeToLive( 22 | lifetime: const Duration(seconds: 12), 23 | ); 24 | expect(cachedValue, const TypeMatcher>()); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/src/dependent_cached_value_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_value/cached_value.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | class TestBed { 5 | String name1 = 'Elon Musk'; 6 | String name2 = 'Jeff Bezos'; 7 | late final marriedNameCache = CachedValue( 8 | () => '${name1.split(' ').first} ${name2.split(' ').last}', 9 | ).withDependency( 10 | () sync* { 11 | yield name1; 12 | yield name2; 13 | }, 14 | ); 15 | } 16 | 17 | void main() { 18 | late TestBed testBed; 19 | 20 | setUp(() { 21 | testBed = TestBed(); 22 | }); 23 | 24 | test( 25 | 'cached value should be computed and cached', 26 | () { 27 | final cachedValueAtStart = testBed.marriedNameCache.value; 28 | 29 | // update value 30 | testBed.name1 = 'Mark Zuckerberg'; 31 | final cachedValueAfterUpdate = testBed.marriedNameCache.value; 32 | 33 | expect(cachedValueAtStart, equals('Elon Bezos')); 34 | expect(cachedValueAfterUpdate, equals('Mark Bezos')); 35 | }, 36 | ); 37 | 38 | test( 39 | 'cached value with multiple dependencies should be computed and cached', 40 | () { 41 | final cachedValueAtStart = testBed.marriedNameCache.value; 42 | 43 | // update value 44 | testBed.name2 = 'Mark Zuckerberg'; 45 | final cachedValueAfterUpdate = testBed.marriedNameCache.value; 46 | 47 | expect(cachedValueAtStart, equals('Elon Bezos')); 48 | expect(cachedValueAfterUpdate, equals('Elon Zuckerberg')); 49 | }, 50 | ); 51 | 52 | test( 53 | 'dependency change should update cached value on next access', 54 | () { 55 | final cachedValueAtStart = testBed.marriedNameCache.value; 56 | final validAfterFirstAccess = testBed.marriedNameCache.isValid; 57 | 58 | // update value 59 | testBed.name1 = 'Mark Zuckerberg'; 60 | final validAfterUpdate = testBed.marriedNameCache.isValid; 61 | final cachedValueAfterUpdate = testBed.marriedNameCache.value; 62 | final validAfterUpdateAccess = testBed.marriedNameCache.isValid; 63 | 64 | expect(cachedValueAtStart, equals('Elon Bezos')); 65 | expect(cachedValueAfterUpdate, equals('Mark Bezos')); 66 | 67 | expect(validAfterFirstAccess, isTrue); 68 | expect(validAfterUpdate, isFalse); 69 | expect(validAfterUpdateAccess, isTrue); 70 | }, 71 | ); 72 | 73 | test( 74 | 'invalidate should update cached value on next access', 75 | () { 76 | // first access 77 | final cachedValueStart = testBed.marriedNameCache.value; 78 | final validAfterFirstAccess = testBed.marriedNameCache.isValid; 79 | 80 | // invalidate 81 | testBed.marriedNameCache.invalidate(); 82 | final validAfterInvalidate = testBed.marriedNameCache.isValid; 83 | 84 | // second access 85 | final cachedValueAfterInvalidate = testBed.marriedNameCache.value; 86 | final validAfterInvalidateAccess = testBed.marriedNameCache.isValid; 87 | 88 | expect(cachedValueStart, equals('Elon Bezos')); 89 | expect(cachedValueAfterInvalidate, equals('Elon Bezos')); 90 | 91 | expect(validAfterFirstAccess, isTrue); 92 | expect(validAfterInvalidate, isFalse); 93 | expect(validAfterInvalidateAccess, isTrue); 94 | }, 95 | ); 96 | 97 | test('refresh should update cached value immediately', () { 98 | // first access 99 | final cachedValueStart = testBed.marriedNameCache.value; 100 | final validAfterFirstAccess = testBed.marriedNameCache.isValid; 101 | 102 | // update value 103 | testBed.name1 = 'Mark Zuckerberg'; 104 | final validAfterUpdate = testBed.marriedNameCache.isValid; 105 | 106 | // refresh 107 | testBed.marriedNameCache.refresh(); 108 | final validAfterRefresh = testBed.marriedNameCache.isValid; 109 | 110 | // second access 111 | final cachedValueAfterUpdate = testBed.marriedNameCache.value; 112 | 113 | expect(cachedValueStart, equals('Elon Bezos')); 114 | expect(cachedValueAfterUpdate, equals('Mark Bezos')); 115 | 116 | expect(validAfterFirstAccess, isTrue); 117 | expect(validAfterUpdate, isFalse); 118 | expect(validAfterRefresh, isTrue); 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /test/src/simple_cached_value_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_value/src/cached_value.dart'; 2 | import 'package:meta/meta.dart' show isTest; 3 | import 'package:test/test.dart'; 4 | 5 | class TestBed { 6 | int current = 0; 7 | int updates = 0; 8 | late final cached = CachedValue(() { 9 | updates++; 10 | return current; 11 | }); 12 | } 13 | 14 | @isTest 15 | void testCachedValue( 16 | String description, 17 | dynamic Function(TestBed testBed) body, 18 | ) { 19 | final testBed = TestBed(); 20 | 21 | test(description, () => body(testBed)); 22 | } 23 | 24 | void main() { 25 | testCachedValue('Cached value should be cached', (testBed) { 26 | expect(testBed.current, equals(0)); 27 | expect(testBed.updates, equals(0)); 28 | final cachedValue = testBed.cached.value; 29 | testBed.current = 1; 30 | expect(cachedValue, equals(0)); 31 | expect(testBed.updates, equals(1)); 32 | }); 33 | testCachedValue( 34 | 'invalidate should update cached value on next access', 35 | (testBed) { 36 | // first access 37 | final cachedValueStart = testBed.cached.value; 38 | final updatesAfterFirstAccess = testBed.updates; 39 | final validAfterFirstAccess = testBed.cached.isValid; 40 | 41 | // update value 42 | testBed.current = 100; 43 | final cachedValueAfterUpdate = testBed.cached.value; 44 | 45 | // invalidate 46 | testBed.cached.invalidate(); 47 | final updatesAfterInvalidate = testBed.updates; 48 | final validAfterInvalidate = testBed.cached.isValid; 49 | 50 | // second access 51 | final cachedValueAfterInvalidate = testBed.cached.value; 52 | final updatesAfterInvalidateAccess = testBed.updates; 53 | final validAfterInvalidateAccess = testBed.cached.isValid; 54 | 55 | expect(cachedValueStart, equals(0)); 56 | expect(cachedValueAfterUpdate, equals(0)); 57 | expect(cachedValueAfterInvalidate, equals(100)); 58 | 59 | expect(updatesAfterFirstAccess, equals(1)); 60 | expect(updatesAfterInvalidate, equals(1)); 61 | expect(updatesAfterInvalidateAccess, equals(2)); 62 | 63 | expect(validAfterFirstAccess, isTrue); 64 | expect(validAfterInvalidate, isFalse); 65 | expect(validAfterInvalidateAccess, isTrue); 66 | }, 67 | ); 68 | testCachedValue('refresh should update cached value immediately', (testBed) { 69 | final cachedValueStart = testBed.cached.value; 70 | testBed.current = 100; 71 | final updatesBeforeRefresh = testBed.updates; 72 | final validBeforeRefresh = testBed.cached.isValid; 73 | 74 | // refresh 75 | final refreshReturn = testBed.cached.refresh(); 76 | final updatesAfterRefresh = testBed.updates; 77 | final cachedValueAfterRefresh = testBed.cached.value; 78 | final validAfterRefresh = testBed.cached.isValid; 79 | 80 | expect(cachedValueStart, equals(0)); 81 | expect(refreshReturn, equals(100)); 82 | expect(cachedValueAfterRefresh, equals(100)); 83 | 84 | expect(updatesBeforeRefresh, equals(1)); 85 | expect(updatesAfterRefresh, equals(2)); 86 | 87 | expect(validBeforeRefresh, isTrue); 88 | expect(validAfterRefresh, isTrue); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /test/src/time_to_live_cached_value_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_value/cached_value.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('TimeToLiveCachedValue', () { 6 | test('should wait not compute before lifetime', () async { 7 | var numberOfLifeProblems = 10; 8 | final lifeProblemsAMinuteAgo = CachedValue(() => numberOfLifeProblems) 9 | .withTimeToLive(lifetime: const Duration(seconds: 4)); 10 | 11 | final validAfterDeclaration = lifeProblemsAMinuteAgo.isValid; 12 | final valueAfterDeclaration = lifeProblemsAMinuteAgo.value; 13 | 14 | numberOfLifeProblems = 150; 15 | 16 | final validAfterUpdate = lifeProblemsAMinuteAgo.isValid; 17 | final valueAfterUpdate = lifeProblemsAMinuteAgo.value; 18 | 19 | await Future.delayed(const Duration(seconds: 2)); 20 | 21 | final validAfter2Seconds = lifeProblemsAMinuteAgo.isValid; 22 | final valueAfter2Seconds = lifeProblemsAMinuteAgo.value; 23 | 24 | expect(validAfterDeclaration, true); 25 | expect(valueAfterDeclaration, 10); 26 | expect(validAfterUpdate, true); 27 | expect(valueAfterUpdate, 10); 28 | expect(validAfter2Seconds, true); 29 | expect(valueAfter2Seconds, 10); 30 | }); 31 | test('should recompute after lifetime', () async { 32 | var numberOfLifeProblems = 10; 33 | final lifeProblemsAMinuteAgo = CachedValue(() => numberOfLifeProblems) 34 | .withTimeToLive(lifetime: const Duration(seconds: 2)); 35 | 36 | numberOfLifeProblems = 150; 37 | 38 | await Future.delayed(const Duration(seconds: 3)); 39 | 40 | final validAfter3Seconds = lifeProblemsAMinuteAgo.isValid; 41 | final valueAfter3Seconds = lifeProblemsAMinuteAgo.value; 42 | 43 | numberOfLifeProblems = 9750; 44 | 45 | await Future.delayed(const Duration(seconds: 3)); 46 | 47 | final validAfter6Seconds = lifeProblemsAMinuteAgo.isValid; 48 | final valueAfter6Seconds = lifeProblemsAMinuteAgo.value; 49 | 50 | expect(validAfter3Seconds, false); 51 | expect(valueAfter3Seconds, 150); 52 | expect(validAfter6Seconds, false); 53 | expect(valueAfter6Seconds, 9750); 54 | }); 55 | test('refresh should reset lifetime', () async { 56 | var numberOfLifeProblems = 10; 57 | final lifeProblemsAMinuteAgo = CachedValue(() => numberOfLifeProblems) 58 | .withTimeToLive(lifetime: const Duration(seconds: 4)); 59 | 60 | numberOfLifeProblems = 150; 61 | 62 | await Future.delayed(const Duration(seconds: 2)); 63 | 64 | // before lifetime ends, force refresh 65 | final validAfter2SecondsBeforeRefresh = lifeProblemsAMinuteAgo.isValid; 66 | final valueAfter2SecondsBeforeRefresh = lifeProblemsAMinuteAgo.value; 67 | 68 | final valueAfter2SecondsAfterRefresh = lifeProblemsAMinuteAgo.refresh(); 69 | final validAfter2SecondsAfterRefresh = lifeProblemsAMinuteAgo.isValid; 70 | 71 | numberOfLifeProblems = 1750; 72 | 73 | await Future.delayed(const Duration(seconds: 2)); 74 | 75 | final validAfter4Seconds = lifeProblemsAMinuteAgo.isValid; 76 | final valueAfter4Seconds = lifeProblemsAMinuteAgo.value; 77 | 78 | expect(validAfter2SecondsBeforeRefresh, true); 79 | expect(valueAfter2SecondsBeforeRefresh, 10); 80 | 81 | expect(validAfter2SecondsAfterRefresh, true); 82 | expect(valueAfter2SecondsAfterRefresh, 150); 83 | 84 | expect(validAfter4Seconds, true); 85 | expect(valueAfter4Seconds, 150); 86 | }); 87 | test('prevent duplicated time to live', () { 88 | expect( 89 | () => CachedValue(() => 'lolo') 90 | .withTimeToLive(lifetime: const Duration(seconds: 1)) 91 | .withTimeToLive(lifetime: const Duration(seconds: 1)), 92 | throwsA( 93 | predicate( 94 | (e) => 95 | e is AssertionError && 96 | e.message == 97 | ''' 98 | There is a declaration of a cached value time to live specified more than once''', 99 | ), 100 | ), 101 | ); 102 | }); 103 | test('pile up with dependency', () async { 104 | var numberOfLifeProblems = 10; 105 | final realNumberOfLifeProblemsAMinuteAgo = 106 | CachedValue(() => numberOfLifeProblems * 2) 107 | .withDependency(() => numberOfLifeProblems) 108 | .withTimeToLive(lifetime: const Duration(seconds: 3)); 109 | 110 | final validAfterDeclaration = realNumberOfLifeProblemsAMinuteAgo.isValid; 111 | final valueAfterDeclaration = realNumberOfLifeProblemsAMinuteAgo.value; 112 | 113 | numberOfLifeProblems = 150; 114 | 115 | final validAfterUpdate = realNumberOfLifeProblemsAMinuteAgo.isValid; 116 | final valueAfterUpdate = realNumberOfLifeProblemsAMinuteAgo.value; 117 | 118 | await Future.delayed(const Duration(seconds: 4)); 119 | 120 | final validAfter2Seconds = realNumberOfLifeProblemsAMinuteAgo.isValid; 121 | final valueAfter2Seconds = realNumberOfLifeProblemsAMinuteAgo.value; 122 | 123 | expect(validAfterDeclaration, true); 124 | expect(valueAfterDeclaration, 20); 125 | expect(validAfterUpdate, false); 126 | expect(valueAfterUpdate, 300); 127 | expect(validAfter2Seconds, false); 128 | expect(valueAfter2Seconds, 300); 129 | }); 130 | }); 131 | } 132 | --------------------------------------------------------------------------------