├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── App.dart ├── AppActions.dart ├── AppState.dart ├── ConsoleEffect.dart ├── main.dart └── services.dart ├── lib └── flutter_observable_state.dart ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | .pub/ 7 | ios 8 | android 9 | .metadata 10 | flutter_observable_state.iml 11 | .DS_Store 12 | .idea 13 | build/ 14 | # If you're building an application, you may want to check-in your pubspec.lock 15 | pubspec.lock 16 | 17 | # Directory created by dartdoc 18 | # If you don't generate documentation locally you can remove this line. 19 | doc/api/ 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.3.0 2 | 3 | Updated to latest rxjs 4 | 5 | # 1.1.0+2 6 | 7 | Fix bug with computed and drop StreamBuilder 8 | 9 | # 1.1.0+1 10 | 11 | Fix void typing in Reaction 12 | 13 | # 1.1.0 14 | 15 | Added **Reaction** 16 | 17 | # 1.0.1+1 18 | 19 | Example, description and formatting 20 | 21 | # 1.0.1 22 | 23 | Fix memoryleak related to disposal of **observe** 24 | 25 | # 1.0.0 26 | 27 | Initial relase -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Christian Alfoni 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 | # flutter_observable_state 2 | Observable state for flutter applications 3 | 4 | ## Motivation 5 | Coming from the world of state management in Javascript/Typescript, I felt that the current solutions to state management with Flutter was too verbose. Building libraries like [CerebralJS](https://www.cerebraljs.com) and [OvermindJS](https://overmindjs.org), I took inspiration from that work and built this simple approach to managing state in Flutter. 6 | 7 | ## The concept 8 | 9 | ```dart 10 | class AppState { 11 | final count = Observable(0); 12 | } 13 | ``` 14 | 15 | Now you have an observable piece of state, lets use it in a widget. 16 | 17 | ```dart 18 | // For simplicity we just instantiate, please read 19 | // further for proper setup 20 | final _state = AppState() 21 | 22 | class MyWidget extends StatelessWidget { 23 | @override 24 | Widget build(context) { 25 | return Container( 26 | child: observe(() => ( 27 | Row( 28 | children: [ 29 | Text(_state.count.get()), 30 | FlatButton( 31 | onPressed: () { 32 | _state.count.change((count) => count + 1) 33 | }, 34 | child: Text("Increase count") 35 | ) 36 | ] 37 | ) 38 | )) 39 | ) 40 | } 41 | } 42 | ``` 43 | 44 | Any widgets returned within the scope of the `observe` function will rerender when any state it accesses changes. You can use as many `observe` you want within a widget, even nest them. 45 | 46 | ## Organizing your project 47 | 48 | When you think about state as application state you will rather define and change the state of the application outside of your widgets. You will still need local widget state, but you primarily want to put your state outside the widgets. To effectively share this state and the logic to change it with all widgets of your application it is highly recommended to use the [get_it](https://pub.dev/packages/get_it) project. Let us create our initial setup. 49 | 50 | We want to create two classes. **AppState** and **Actions**. We can use **get_it** to create a single instance of these classes, which can then be used in any widget. 51 | 52 | ```dart 53 | // services.dart 54 | import 'package:my_project/AppState.dart'; 55 | import 'package:my_project/Actions.dart'; 56 | import 'package:get_it/get_it.dart'; 57 | 58 | final getIt = GetIt(); 59 | 60 | void initialize() { 61 | getIt.registerSingleton(AppState()); 62 | getIt.registerSingleton(Actions()); 63 | } 64 | ``` 65 | 66 | You can use this file to also register effects you want to perform. For example classes that manages communication with Firebase etc. 67 | 68 | ```dart 69 | // AppState.dart 70 | import 'package:flutter_observable_state/flutter_observable_state.dart'; 71 | 72 | class AppState { 73 | final count = Observable(0); 74 | } 75 | 76 | // Actions.dart 77 | import 'package:my_project/AppState.dart'; 78 | import 'package:my_project/services.dart'; 79 | 80 | class Actions { 81 | final _state = getIt.get(); 82 | 83 | void changeCount(int count) { 84 | _state.count.change((currentCount) => currentCount + count) 85 | } 86 | } 87 | ``` 88 | 89 | Now in a widget you are able to do: 90 | 91 | ```dart 92 | class MyWidget extends StatelessWidget { 93 | final _state = getIt.get() 94 | final _actions = getIt.get() 95 | 96 | @override 97 | Widget build(context) { 98 | return Container( 99 | child: observe(() => ( 100 | Row( 101 | children: [ 102 | Text(_state.count.get()), 103 | FlatButton( 104 | onPressed: () { 105 | _actions.changeCount(1) 106 | }, 107 | child: Text("Increase count") 108 | ) 109 | ] 110 | ) 111 | )) 112 | ) 113 | } 114 | } 115 | ``` 116 | 117 | We have now effectively allowed any widget to access our count and any widget can change it, making sure that any widget observing the state will rerender. 118 | 119 | ## API 120 | 121 | ### Observable 122 | 123 | Create an observable value. 124 | 125 | ```dart 126 | Observable(0); 127 | Observable> = Observable([]); 128 | Observable(null); 129 | ``` 130 | 131 | ### Observable.get 132 | 133 | Get the value of an **Observable**. 134 | 135 | ```dart 136 | var count = Observable(0); 137 | 138 | count.get(); // 0 139 | ``` 140 | 141 | ### Observable.set 142 | 143 | Set the value of an **Observable**. 144 | 145 | ```dart 146 | var count = Observable(0); 147 | 148 | count.set(1); 149 | ``` 150 | 151 | ### Observable.change 152 | 153 | Change the value of an **Observable**. 154 | 155 | ```dart 156 | var count = Observable(0); 157 | 158 | count.change((currentCount) => currentCount + 1); 159 | ``` 160 | 161 | ### Observable.setStream 162 | 163 | Connect a stream of values, making the **Observable** update whenever the stream passes a new value. 164 | 165 | ```dart 166 | var user = Observable(null); 167 | 168 | user.setStream(FirebaseAuth.instance.onAuthStateChanged); 169 | 170 | // Unset stream 171 | user.setStream(null); 172 | ``` 173 | 174 | When a stream is set you can still **set** and **change** to a new value. 175 | 176 | ### Computed 177 | 178 | You can derive state. This works much like the **observe**, but it only flags the computed as dirty. The next time something **get**s the value, it will be recalculated. 179 | 180 | ```dart 181 | var foo = Observable('bar'); 182 | var upperFoo = Computed(() => foo.get().toUpperCase()); 183 | ``` 184 | 185 | You will typically define computeds with your **AppState** class. 186 | 187 | ```dart 188 | class AppState { 189 | final foo = Observable('bar'); 190 | 191 | Computed upperFoo; 192 | 193 | AppState() { 194 | upperFoo = Computed(() => foo.get().toUpperCase()); 195 | } 196 | } 197 | ``` 198 | 199 | ### observe 200 | 201 | To observe state in widgets you use the **observe** function. It returns a **StreamBuilder** and can be used wherever you typically insert a child widget. 202 | 203 | ```dart 204 | class MyWidget extends StatelessWidget { 205 | final _state = getIt.get(); 206 | 207 | @override 208 | Widget build(context) { 209 | return Container( 210 | child: observe(() => ( 211 | Text(_state.foo.get()) 212 | )) 213 | ) 214 | } 215 | } 216 | ``` 217 | 218 | ### Reaction 219 | 220 | You can observe state and react to it. This is useful when you need to do some imperative logic inside your widgets. For example here we are controlling an overlay from our application state: 221 | 222 | ```dart 223 | class MyWidget extends StatefulWidget { 224 | @override 225 | createState() => MyWidgetState(); 226 | } 227 | 228 | class MyWidgetState extends State { 229 | final _state = getIt.get(); 230 | Reaction reaction; 231 | OverlayEntry overlay; 232 | 233 | @override 234 | void initState() { 235 | reaction = Reaction( 236 | () => _state.isOverlayOpen.get(), 237 | () => { 238 | if (_state.isOverlayOpen.get() && overlay == null) { 239 | overlay = _createOverlayEntry(); 240 | Overlay.of(context).insert(overlay); 241 | } else if (!_state.isOverlayOpen.get() && overlay != null) { 242 | overlay.remove(); 243 | overlay = null; 244 | } 245 | } 246 | ) 247 | super.initState(); 248 | } 249 | 250 | @override 251 | Widget build(context) { 252 | return Container( 253 | child: observe(() => ( 254 | Text(_state.foo.get()) 255 | )) 256 | ) 257 | } 258 | } 259 | ``` 260 | 261 | ## Models 262 | 263 | You can use **Observable** with whatever classes you want, even inside widgets. Typically though you want to use it with classes representing models. For example you want to track optimistically adding a like to posts. 264 | 265 | ```dart 266 | // Post.dart 267 | class Post { 268 | String id; 269 | String title; 270 | String description; 271 | Observable likesCount; 272 | 273 | Post.fromJSON(Map json) : 274 | id = json["id"], 275 | title = json["title"], 276 | description = json["description"], 277 | likesCount = Observable(json["likesCount"]); 278 | } 279 | 280 | // AppState.dart 281 | class AppState { 282 | final posts = Observable>([]); 283 | } 284 | 285 | // Actions.dart 286 | class Actions { 287 | final _state = getIt.get(); 288 | final _api = getIt.get(); 289 | 290 | void initialize() { 291 | _state.posts.setStream(_api.$posts); 292 | } 293 | 294 | void likePost(Post post) { 295 | post.likesCount.change((likesCount) => likesCount + 1); 296 | _api.likePost(post.id); 297 | } 298 | } 299 | ``` 300 | 301 | ## How does it work? 302 | 303 | Dart is a single threaded language, meaning that only one **observe** can run at any time. That means the library orchestrates the execution of **observe** and **Computed** with the execution of any **Observable.get** globally. The **Computed** are considered to live as long as the application lives, while **observe** uses the **StreamBuilder** where it clears out existing subscriptions when it builds and when the widget is disposed. -------------------------------------------------------------------------------- /example/App.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_observable_state/flutter_observable_state.dart'; 3 | 4 | import 'AppActions.dart'; 5 | import 'AppState.dart'; 6 | import 'services.dart'; 7 | 8 | class App extends StatelessWidget { 9 | final _state = getIt.get(); 10 | final _actions = getIt.get(); 11 | 12 | @override 13 | Widget build(context) { 14 | return MaterialApp( 15 | title: 'Example', 16 | home: observe(() => (Center( 17 | child: Row( 18 | mainAxisAlignment: MainAxisAlignment.spaceAround, 19 | children: [ 20 | Text(_state.count.get().toString()), 21 | FlatButton( 22 | onPressed: () { 23 | _actions.increaseCount(); 24 | }, 25 | child: Text("Increase")) 26 | ]))))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/AppActions.dart: -------------------------------------------------------------------------------- 1 | import 'AppState.dart'; 2 | import 'ConsoleEffect.dart'; 3 | import 'services.dart'; 4 | 5 | class AppActions { 6 | final _state = getIt.get(); 7 | final _console = getIt.get(); 8 | 9 | void increaseCount() { 10 | _state.count.change((count) => count + 1); 11 | _console.log("Count increased!"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/AppState.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_observable_state/flutter_observable_state.dart'; 2 | 3 | class AppState { 4 | final count = Observable(0); 5 | } 6 | -------------------------------------------------------------------------------- /example/ConsoleEffect.dart: -------------------------------------------------------------------------------- 1 | class ConsoleEffect { 2 | log(String message) { 3 | print(message); 4 | } 5 | 6 | info(String message) { 7 | print("INFO: " + message); 8 | } 9 | 10 | error(String message) { 11 | print("ERROR: " + message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import './services.dart' as services; 3 | import './App.dart'; 4 | 5 | void main() { 6 | services.initialize(); 7 | 8 | runApp(App()); 9 | } 10 | -------------------------------------------------------------------------------- /example/services.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | 3 | import 'AppActions.dart'; 4 | import 'AppState.dart'; 5 | import 'ConsoleEffect.dart'; 6 | 7 | final getIt = GetIt(); 8 | 9 | void initialize() { 10 | getIt.registerSingleton(AppState()); 11 | getIt.registerSingleton(ConsoleEffect()); 12 | getIt.registerSingleton(AppActions()); 13 | } 14 | -------------------------------------------------------------------------------- /lib/flutter_observable_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:rxdart/rxdart.dart'; 4 | 5 | Observer currentObserver; 6 | 7 | class Observer { 8 | Map _subscriptions = Map(); 9 | BehaviorSubject _subject; 10 | 11 | Observer() { 12 | _subject = BehaviorSubject.seeded(null); 13 | _subject.onCancel = () { 14 | _clear(); 15 | }; 16 | } 17 | 18 | _clear() { 19 | _subscriptions.forEach((observable, subscription) { 20 | subscription.cancel(); 21 | }); 22 | _subscriptions.clear(); 23 | } 24 | 25 | addListener(Stream rxObservable) { 26 | if (_subscriptions.containsKey(rxObservable)) { 27 | return; 28 | } 29 | 30 | _subscriptions[rxObservable] = rxObservable.skip(1).listen((data) { 31 | _subject.add(data); 32 | }); 33 | } 34 | } 35 | 36 | class ObserverWidget extends StatefulWidget { 37 | final Widget Function() cb; 38 | ObserverWidget(this.cb); 39 | State createState() => _ObserverWidgetState(); 40 | } 41 | 42 | class _ObserverWidgetState extends State { 43 | Observer _observer; 44 | StreamSubscription _listenSubscription; 45 | bool _isMounted = false; 46 | 47 | _ObserverWidgetState() { 48 | _observer = Observer(); 49 | } 50 | 51 | @override 52 | void initState() { 53 | _listenSubscription = _observer._subject.stream.listen((data) { 54 | if (_isMounted) { 55 | setState(() {}); 56 | } 57 | }); 58 | _isMounted = true; 59 | super.initState(); 60 | } 61 | 62 | @override 63 | void dispose() { 64 | _isMounted = false; 65 | _listenSubscription.cancel(); 66 | _observer._clear(); 67 | super.dispose(); 68 | } 69 | 70 | @override 71 | Widget build(context) { 72 | _observer._clear(); 73 | 74 | final observer = currentObserver; 75 | currentObserver = this._observer; 76 | final result = widget.cb(); 77 | currentObserver = observer; 78 | 79 | return result; 80 | } 81 | } 82 | 83 | class Reaction extends Observer { 84 | dynamic Function() trackCb; 85 | void Function() cb; 86 | 87 | Reaction(this.trackCb, this.cb) : super() { 88 | _subject.stream.skip(1).listen((_) { 89 | cb(); 90 | }); 91 | 92 | var previousObserver = currentObserver; 93 | currentObserver = this; 94 | trackCb(); 95 | currentObserver = previousObserver; 96 | } 97 | 98 | void dispose() { 99 | _clear(); 100 | } 101 | } 102 | 103 | Widget observe(Widget Function() cb) { 104 | return ObserverWidget(cb); 105 | } 106 | 107 | class Observable { 108 | StreamSubscription _stream; 109 | BehaviorSubject _subject; 110 | Stream get $stream => _subject.stream; 111 | 112 | T get() { 113 | if (currentObserver != null) { 114 | currentObserver.addListener($stream); 115 | } 116 | 117 | return _subject.value; 118 | } 119 | 120 | void setStream(Stream stream) { 121 | if (_stream != null) { 122 | _stream.cancel(); 123 | } 124 | 125 | if (stream == null) { 126 | return; 127 | } 128 | 129 | _stream = stream.listen((value) => _subject.add(value)); 130 | } 131 | 132 | void set(T newValue) { 133 | _subject.add(newValue); 134 | } 135 | 136 | void change(T Function(T) cb) { 137 | _subject.add(cb(_subject.value)); 138 | } 139 | 140 | Observable(T initialValue) { 141 | this._subject = BehaviorSubject.seeded(initialValue); 142 | } 143 | } 144 | 145 | class Computed extends Observer { 146 | T Function() cb; 147 | bool _isDirty = true; 148 | dynamic _cachedResult; 149 | 150 | Computed(this.cb) : super() { 151 | _subject.stream.listen((_) { 152 | _isDirty = true; 153 | }); 154 | } 155 | 156 | T get() { 157 | if (_isDirty) { 158 | _clear(); 159 | var previousObserver = currentObserver; 160 | currentObserver = this; 161 | _cachedResult = cb(); 162 | currentObserver = previousObserver; 163 | _isDirty = false; 164 | } 165 | 166 | if (currentObserver != null) { 167 | final observer = currentObserver; 168 | currentObserver.addListener(_subject); 169 | /* 170 | observer._subscriptions[_subject.stream] = _subject.stream.listen((data) { 171 | observer._subject.add(data); 172 | }); 173 | */ 174 | } 175 | 176 | return _cachedResult; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_observable_state 2 | description: Simple and powerful state management for Flutter applications. With minimal boilerplate, stream support and computed state 3 | version: 1.3.0 4 | homepage: https://github.com/christianalfoni/flutter_observable_state 5 | environment: 6 | sdk: '>=2.1.0 <3.0.0' 7 | dependencies: 8 | flutter: 9 | sdk: flutter 10 | rxdart: ^0.24.1 11 | dev_dependencies: 12 | flutter_test: 13 | sdk: flutter 14 | get_it: 1.0.3+1 15 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:flutter_observable_state/flutter_observable_state.dart'; 11 | import 'package:rxdart/rxdart.dart' as rx; 12 | 13 | class TestWidget extends StatelessWidget { 14 | Widget observer; 15 | 16 | TestWidget(this.observer); 17 | 18 | @override 19 | Widget build(context) { 20 | return Container(child: this.observer); 21 | } 22 | } 23 | 24 | class ReactionWidget extends StatefulWidget { 25 | Function trackCb; 26 | Function cb; 27 | 28 | ReactionWidget(this.trackCb, this.cb); 29 | 30 | createState() => ReactionWidgetState(); 31 | } 32 | 33 | class ReactionWidgetState extends State { 34 | Reaction reaction; 35 | 36 | @override 37 | void initState() { 38 | reaction = Reaction(widget.trackCb, widget.cb); 39 | super.initState(); 40 | } 41 | 42 | @override 43 | void dispose() { 44 | reaction.dispose(); 45 | super.dispose(); 46 | } 47 | 48 | @override 49 | Widget build(context) { 50 | return Container(); 51 | } 52 | } 53 | 54 | class AppState { 55 | final foo = Observable("bar"); 56 | Computed upperFoo; 57 | 58 | AppState() { 59 | upperFoo = Computed(() => foo.get().toUpperCase()); 60 | } 61 | } 62 | 63 | void main() { 64 | testWidgets('Updates when setting state', (WidgetTester tester) async { 65 | final state = AppState(); 66 | 67 | await tester.pumpWidget(TestWidget(observe(() { 68 | return Text(state.foo.get(), textDirection: TextDirection.ltr); 69 | }))); 70 | 71 | expect(find.text('bar'), findsOneWidget); 72 | 73 | state.foo.set("bar2"); 74 | await tester.pump(Duration.zero); 75 | 76 | expect(find.text('bar2'), findsOneWidget); 77 | }); 78 | 79 | testWidgets('Updates only once setting state synchronously', 80 | (WidgetTester tester) async { 81 | final state = AppState(); 82 | int runCount = 0; 83 | 84 | await tester.pumpWidget(TestWidget(observe(() { 85 | runCount++; 86 | return Text(state.foo.get(), textDirection: TextDirection.ltr); 87 | }))); 88 | 89 | expect(find.text('bar'), findsOneWidget); 90 | 91 | state.foo.set("bar2"); 92 | state.foo.set("bar3"); 93 | await tester.pump(Duration(seconds: 1)); 94 | 95 | expect(find.text('bar3'), findsOneWidget); 96 | expect(runCount, 2); 97 | }); 98 | 99 | testWidgets('Updates when changing state', (WidgetTester tester) async { 100 | final state = AppState(); 101 | 102 | await tester.pumpWidget(TestWidget(observe(() { 103 | return Text(state.foo.get(), textDirection: TextDirection.ltr); 104 | }))); 105 | 106 | expect(find.text('bar'), findsOneWidget); 107 | 108 | state.foo.change((text) => text.toUpperCase()); 109 | await tester.pump(Duration.zero); 110 | 111 | expect(find.text('BAR'), findsOneWidget); 112 | }); 113 | 114 | testWidgets('Updates when changing state', (WidgetTester tester) async { 115 | final state = AppState(); 116 | 117 | await tester.pumpWidget(TestWidget(observe(() { 118 | return Text(state.foo.get(), textDirection: TextDirection.ltr); 119 | }))); 120 | 121 | expect(find.text('bar'), findsOneWidget); 122 | 123 | state.foo.change((text) => text.toUpperCase()); 124 | await tester.pump(Duration.zero); 125 | 126 | expect(find.text('BAR'), findsOneWidget); 127 | }); 128 | 129 | testWidgets('Updates when stream updates', (WidgetTester tester) async { 130 | final state = AppState(); 131 | var stream = rx.BehaviorSubject(); 132 | 133 | state.foo.setStream(stream); 134 | 135 | await tester.pumpWidget(TestWidget(observe(() { 136 | return Text(state.foo.get(), textDirection: TextDirection.ltr); 137 | }))); 138 | 139 | expect(find.text('bar'), findsOneWidget); 140 | 141 | stream.add("bar2"); 142 | await tester.pump(Duration.zero); 143 | 144 | expect(find.text('bar2'), findsOneWidget); 145 | }); 146 | 147 | testWidgets('Computed state', (WidgetTester tester) async { 148 | final state = AppState(); 149 | 150 | await tester.pumpWidget(TestWidget(observe(() { 151 | return Text(state.upperFoo.get(), textDirection: TextDirection.ltr); 152 | }))); 153 | 154 | expect(find.text('BAR'), findsOneWidget); 155 | 156 | state.foo.set("baz"); 157 | await tester.pump(Duration.zero); 158 | 159 | expect(find.text('BAZ'), findsOneWidget); 160 | }); 161 | 162 | testWidgets('Reaction', (WidgetTester tester) async { 163 | final state = AppState(); 164 | var runCount = 0; 165 | 166 | await tester.pumpWidget(ReactionWidget(() => state.foo.get(), () { 167 | runCount++; 168 | })); 169 | 170 | expect(runCount, 0); 171 | 172 | state.foo.set("baz"); 173 | await tester.pump(Duration.zero); 174 | 175 | expect(runCount, 1); 176 | 177 | await tester.pumpWidget(Container()); 178 | 179 | state.foo.set("baz2"); 180 | await tester.pump(Duration.zero); 181 | 182 | expect(runCount, 1); 183 | }); 184 | } 185 | --------------------------------------------------------------------------------