├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── lib ├── combiners.dart ├── flow.dart ├── flow_converters.dart ├── ordered_executor.dart ├── replay_stream.dart ├── sequential_executor.dart ├── stream_event.dart └── widgets.dart ├── pubspec.yaml └── test ├── combiners_test.dart ├── flow_converters_test.dart ├── flow_test.dart ├── ordered_executor_test.dart ├── replay_stream_test.dart ├── sequential_executor_test.dart └── widgets_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool 2 | pubspec.lock 3 | build/* 4 | BUILD 5 | METADATA 6 | OWNERS 7 | copy.bara.sky 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This product is supported for internal google use and is offered externally 2 | "as is". External contributions/bug reports/etc are not taken at this time. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Utility package that makes stream-based programming in Flutter easier to reason 2 | about. 3 | 4 | This product is supported for internal google use and is offered externally 5 | "as is". External contributions/bug reports/etc are not taken at this time. -------------------------------------------------------------------------------- /lib/combiners.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Utility combiners based on `package:stream_transform`. 16 | /// 17 | /// All methods delegate to the [combineLatest] function. 18 | 19 | import 'dart:async'; 20 | 21 | import 'package:stream_transform/stream_transform.dart'; 22 | import 'package:tuple/tuple.dart'; 23 | 24 | import 'flow_converters.dart'; 25 | 26 | /// Combines the latest values from [t1] and [t2] using [combiner]. 27 | /// 28 | /// This is just syntactic sugar for [transform()] and [combineLatest()]. 29 | Stream combineLatest2( 30 | Stream t1, 31 | Stream t2, 32 | FutureOr Function(T1, T2) combiner, 33 | ) => 34 | t1.combineLatest(t2, combiner); 35 | 36 | /// Combines the latest values from [t1] and [t2] and returns a tuple stream. 37 | /// 38 | /// This method delegates to [combineLatest2()]. 39 | Stream> combineLatestTuple2( 40 | Stream t1, 41 | Stream t2, 42 | ) => 43 | combineLatest2(t1, t2, convertToTuple2); 44 | 45 | /// Combines the latest values from [t1], [t2], and [t3] using [combiner]. 46 | /// 47 | /// This is just syntactic sugar for [transform()] and [combineLatest()]. 48 | Stream combineLatest3( 49 | Stream t1, 50 | Stream t2, 51 | Stream t3, 52 | FutureOr Function(T1, T2, T3) combiner, 53 | ) => 54 | combineLatest2, T3, Output>( 55 | combineLatestTuple2(t1, t2), 56 | t3, 57 | (tuple, v3) => combiner(tuple.item1, tuple.item2, v3), 58 | ); 59 | 60 | /// Combines the latest values from [t1], [t2], and [t3] and returns a tuple 61 | /// stream. 62 | /// 63 | /// This method delegates to [combineLatest3()]. 64 | Stream> combineLatestTuple3( 65 | Stream t1, 66 | Stream t2, 67 | Stream t3, 68 | ) => 69 | combineLatest3(t1, t2, t3, convertToTuple3); 70 | 71 | /// Combines the latest values from [t1], [t2], [t3], and [t4] using [combiner]. 72 | /// 73 | /// This is just syntactic sugar for [transform()] and [combineLatest()]. 74 | Stream combineLatest4( 75 | Stream t1, 76 | Stream t2, 77 | Stream t3, 78 | Stream t4, 79 | FutureOr Function(T1, T2, T3, T4) combiner, 80 | ) => 81 | combineLatest2, T4, Output>( 82 | combineLatestTuple3(t1, t2, t3), 83 | t4, 84 | (tuple, v4) => combiner(tuple.item1, tuple.item2, tuple.item3, v4), 85 | ); 86 | 87 | /// Combines the latest values from [t1], [t2], [t3], and [t4] and returns a 88 | /// tuple stream. 89 | /// 90 | /// This method delegates to [combineLatest4()]. 91 | Stream> combineLatestTuple4( 92 | Stream t1, 93 | Stream t2, 94 | Stream t3, 95 | Stream t4, 96 | ) => 97 | combineLatest4(t1, t2, t3, t4, convertToTuple4); 98 | 99 | /// Combines the latest values from [t1], [t2], [t3], [t4], and [t5] using 100 | /// [combiner]. 101 | /// 102 | /// This is just syntactic sugar for [transform()] and [combineLatest()]. 103 | Stream combineLatest5( 104 | Stream t1, 105 | Stream t2, 106 | Stream t3, 107 | Stream t4, 108 | Stream t5, 109 | FutureOr Function(T1, T2, T3, T4, T5) combiner, 110 | ) => 111 | combineLatest2, T5, Output>( 112 | combineLatestTuple4(t1, t2, t3, t4), 113 | t5, 114 | (tuple, v5) => 115 | combiner(tuple.item1, tuple.item2, tuple.item3, tuple.item4, v5), 116 | ); 117 | 118 | /// Combines the latest values from [t1], [t2], [t3], [t4], and [t5] and returns 119 | /// a tuple stream. 120 | /// 121 | /// This method delegates to [combineLatest5()]. 122 | Stream> combineLatestTuple5( 123 | Stream t1, 124 | Stream t2, 125 | Stream t3, 126 | Stream t4, 127 | Stream t5, 128 | ) => 129 | combineLatest5(t1, t2, t3, t4, t5, convertToTuple5); 130 | 131 | /// Combines the latest values from [t1], [t2], [t3], [t4], [t5], and [t6] using 132 | /// [combiner]. 133 | /// 134 | /// This is just syntactic sugar for [transform()] and [combineLatest()]. 135 | Stream combineLatest6( 136 | Stream t1, 137 | Stream t2, 138 | Stream t3, 139 | Stream t4, 140 | Stream t5, 141 | Stream t6, 142 | FutureOr Function(T1, T2, T3, T4, T5, T6) combiner, 143 | ) => 144 | combineLatest2, T6, Output>( 145 | combineLatestTuple5(t1, t2, t3, t4, t5), 146 | t6, 147 | (tuple, v6) => combiner( 148 | tuple.item1, 149 | tuple.item2, 150 | tuple.item3, 151 | tuple.item4, 152 | tuple.item5, 153 | v6, 154 | ), 155 | ); 156 | 157 | /// Combines the latest values from [t1], [t2], [t3], [t4], [t5], and [t6] and 158 | /// returns a tuple stream. 159 | /// 160 | /// This method delegates to [combineLatest6()]. 161 | Stream> 162 | combineLatestTuple6( 163 | Stream t1, 164 | Stream t2, 165 | Stream t3, 166 | Stream t4, 167 | Stream t5, 168 | Stream t6, 169 | ) => 170 | combineLatest6(t1, t2, t3, t4, t5, t6, convertToTuple6); 171 | 172 | /// Combines the latest values from [t1], [t2], [t3], [t4], [t5], [t6] and [t7] 173 | /// using [combiner]. 174 | /// 175 | /// This is just syntactic sugar for [transform()] and [combineLatest()]. 176 | Stream combineLatest7( 177 | Stream t1, 178 | Stream t2, 179 | Stream t3, 180 | Stream t4, 181 | Stream t5, 182 | Stream t6, 183 | Stream t7, 184 | FutureOr Function(T1, T2, T3, T4, T5, T6, T7) combiner, 185 | ) => 186 | combineLatest2, T7, Output>( 187 | combineLatestTuple6(t1, t2, t3, t4, t5, t6), 188 | t7, 189 | (tuple, v7) => combiner( 190 | tuple.item1, 191 | tuple.item2, 192 | tuple.item3, 193 | tuple.item4, 194 | tuple.item5, 195 | tuple.item6, 196 | v7, 197 | ), 198 | ); 199 | 200 | /// Combines the latest values from [t1], [t2], [t3], [t4], [t5], [t6], and [t7] 201 | /// and returns a tuple stream. 202 | /// 203 | /// This method delegates to [combineLatest7()]. 204 | Stream> 205 | combineLatestTuple7( 206 | Stream t1, 207 | Stream t2, 208 | Stream t3, 209 | Stream t4, 210 | Stream t5, 211 | Stream t6, 212 | Stream t7, 213 | ) => 214 | combineLatest7(t1, t2, t3, t4, t5, t6, t7, convertToTuple7); 215 | -------------------------------------------------------------------------------- /lib/flow.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// A library that makes stream transformer based data flow easier to reason 16 | /// about. 17 | /// 18 | /// In a [Stream]-heavy class, there is often a mixture of [Stream], 19 | /// [StreamController], and various types of stream transformers glued together 20 | /// with [Stream.transform] or methods from `combiners.dart`. This `flow` 21 | /// library encapsulates common patterns and provides a single [Node] class. A 22 | /// [Node] represents a single stage of computation where zero or more [Stream]s 23 | /// are the input and a single [Stream] is the output. 24 | /// 25 | /// By wiring multiple [Node]s together through pure functions, they form a 26 | /// flow of data. 27 | 28 | import 'dart:async'; 29 | 30 | import 'package:auto_disposable/auto_disposable.dart'; 31 | import 'package:quiver/check.dart'; 32 | 33 | import 'combiners.dart'; 34 | import 'replay_stream.dart'; 35 | import 'stream_event.dart'; 36 | 37 | /// A node in a data flow graph that remains idle until wired and used. 38 | /// 39 | /// After you construct a [Node], call one of the [wire*] methods to configure 40 | /// wiring. Wiring logic is always lazily executed until this [Node] has an 41 | /// actual user. Specifically, a [Node] is considered used after the first call 42 | /// to any of: 43 | /// - [value]/[error] if this is settable. 44 | /// - [stream] getter. 45 | /// - [subscribePermanently]. 46 | /// - [subscribeToDataPermanently]. 47 | /// 48 | /// To fetch the latest stream event, simply use the [stream] getter. 49 | /// 50 | /// ```dart 51 | /// final latestEvent = myNode.stream.event; 52 | /// ``` 53 | /// 54 | /// See [ReplayStream] for more details. 55 | /// 56 | /// If a [Node] depends on one or more other [Node]s, for example: 57 | /// 58 | /// ```dart 59 | /// final myNode = Node()..wire2(node1, node2, converter); 60 | /// ``` 61 | /// 62 | /// When `myNode` is first used, the wiring logic of `node1` and `node2` will be 63 | /// executed if they have not been. 64 | /// 65 | /// # Modifiers 66 | /// 67 | /// You may modify the properties of a [Node] before using it. There are two 68 | /// methods. 69 | /// 70 | /// ## [asSettable] 71 | /// 72 | /// Allows this [Node] to be settable. This modifier makes sure that the 73 | /// [value] and [error] setters are properly configured when this [Node] is 74 | /// wired. 75 | /// 76 | /// ## [withInitialEvent] 77 | /// 78 | /// Allows an initial event to be set to [stream]. 79 | /// 80 | /// # Wiring Methods 81 | /// 82 | /// The [Node] class supports a number of wiring methods. They all start with 83 | /// `wire`. Here is the full list: 84 | /// - [wireAsPassthrough] 85 | /// - [wireAsPassthroughNode] 86 | /// - [wireAsTransformedStream] 87 | /// - [wireAsTransformedNode] 88 | /// - [wire0] 89 | /// - [wire1] 90 | /// - [wire2] 91 | /// - [wire3] 92 | /// - [wire4] 93 | /// - [wire5] 94 | /// - [wire6] 95 | /// - [wire7] 96 | /// 97 | /// A specific [Node] object can only accept a single wiring method call, which 98 | /// updates the object from the 'ReadyToConfigure' state to the 'ReadyToWire' 99 | /// state. 100 | /// 101 | /// See `Lifecycle of a Node` section for more details. 102 | /// 103 | /// # Usage Methods 104 | /// 105 | /// A usage method makes use of the [Node]'s output. A [Node] must already be 106 | /// configured or wired before any usage method can be called. Currently, [Node] 107 | /// supports the following usage methods: 108 | /// - [value]/[error] only when the [Node] is settable. 109 | /// - [view] getter. 110 | /// - [stream] getter. 111 | /// - [subscribePermanently]. 112 | /// - [subscribeToDataPermanently]. 113 | /// 114 | /// # Lifecycle of a [Node] 115 | /// 116 | /// Here is a graph showing the lifecycle of a [Node]: 117 | /// 118 | /// *ReadyToConfigure* 119 | /// | 120 | /// | wireAs*, wire0, wire1, wire2, ... 121 | /// V 122 | /// *ReadyToWire* 123 | /// | 124 | /// | first usage call (subscribe*, stream, value, error, view) 125 | /// V 126 | /// *Wired* 127 | /// | 128 | /// | any number of usage calls 129 | /// | 130 | /// | dispose 131 | /// V 132 | /// *Disposed* 133 | /// 134 | /// As soon as a [Node] object is constructed, it is ready to configure. At this 135 | /// stage, no usage method can be called as the [Node] has not been configured 136 | /// or wired. 137 | /// 138 | /// User code must call a SINGLE wiring method to configure the [Node]. A [Node] 139 | /// cannot be wired more than once. After this call, the [Node] is updated to 140 | /// the `ReadyToWire` state. 141 | /// 142 | /// Because wiring is done lazily, the [Node] remains idle until the first usage 143 | /// call, which invokes the necessary logic to actually wire this [Node]. At 144 | /// this point this [Node] becomes wired and all stream subscriptions/transforms 145 | /// are operational. Usage methods can be called an unlimited number of times 146 | /// until this [Node] is disposed of. 147 | class Node with AutoDisposerMixin { 148 | /// Constructs a [Node]. 149 | Node() { 150 | autoDisposeCustom(() { 151 | _stream?.dispose(); 152 | _stream = null; 153 | _wiringCallback = null; 154 | }); 155 | } 156 | 157 | /// Returns `true` if this [Node] is configured as a settable. 158 | bool get settable => _settable; 159 | bool _settable = false; 160 | 161 | /// Returns a read-only view of this [Node]. 162 | /// 163 | /// If this node is not settable, returns itself. 164 | /// 165 | /// If this node is settable, returns a passthrough [Node] that emits the same 166 | /// events. Multiple calls to this getter return the same [Node] instance. 167 | /// 168 | /// This is a usage API - this [Node] will be wired if it has not been. 169 | Node get view { 170 | _ensureWired(); 171 | _view ??= _newView; 172 | return _view!; 173 | } 174 | 175 | Node get _newView { 176 | if (!settable) return this; 177 | final viewNode = Node()..wireAsPassthroughNode(this); 178 | autoDispose(viewNode); 179 | return viewNode; 180 | } 181 | 182 | Node? _view; 183 | 184 | /// An optional initial event passed to [stream] when this [Node] is wired. 185 | StreamEvent? _initialEvent; 186 | 187 | /// Optional function to enable the [value] and [error] setters. 188 | /// 189 | /// This function is set by one of the [wire*] methods when this [Node] is 190 | /// constructed as a settable. 191 | void Function(StreamEvent event)? _setter; 192 | 193 | /// Output of this [Node] that remains `null` until this [Node] is configured 194 | /// and wired. 195 | ReplayStream? _stream; 196 | 197 | /// Callback that actually sets up the wiring of this [Node]. 198 | /// 199 | /// During configuration (via one of the [wire*] methods), this callback 200 | /// becomes `non-null`. The first time this [Node] is used, this callback is 201 | /// invoked to set up the wiring. After this is called, [_stream] and 202 | /// optionally '_setter' become `non-null`. See [_ensureWired]. 203 | void Function()? _wiringCallback; 204 | 205 | /// Output of this [Node]. 206 | /// 207 | /// Requires that this [Node]: 208 | /// - has been configured via one of the [wire*] methods; OR 209 | /// - has been wired. 210 | ReplayStream get stream { 211 | _ensureWired(); 212 | return _stream!; 213 | } 214 | 215 | /// Sets the current value to be added to the output [stream]. 216 | /// 217 | /// Requires that this [Node] is constructed as a settable and that it has 218 | /// been configured or wired. 219 | set value(T value) { 220 | _ensureWired(); 221 | checkState(_setter != null, message: '$this is not settable'); 222 | _setter!(StreamEvent.data(value)); 223 | } 224 | 225 | /// Sets the current error to be added to the output [stream]. 226 | /// 227 | /// Requires that this [Node] is constructed as a settable and that it has 228 | /// been configured or wired. 229 | set error(Object error) { 230 | _ensureWired(); 231 | checkState(_setter != null, message: '$this is not settable'); 232 | _setter!(StreamEvent.error(error)); 233 | } 234 | 235 | /// Subscribes to the output of this [Node] permanently. 236 | /// 237 | /// The subscription is canceled only when this [Node] is disposed of. One can 238 | /// still subscribe to [stream] manually if it is desired to cancel their 239 | /// subscription before disposal. 240 | /// 241 | /// This method is useful when some of your data flow has more complex 242 | /// side effects than emitting to another stream or node. Because it is part 243 | /// of the data flow, the subscription is only canceled when the entire data 244 | /// flow is disposed of. With [subscribePermanently], you won't have to think 245 | /// about the disposal state. 246 | /// 247 | /// Requires that this [Node]: 248 | /// - has been configured via one of the [wire*] methods; OR 249 | /// - has been wired. 250 | void subscribePermanently(void Function(StreamEvent event) callback) { 251 | _ensureWired(); 252 | final subscriber = _stream!.listen( 253 | (data) => callback(StreamEvent.data(data)), 254 | onError: (error, stackTrace) => 255 | callback(StreamEvent.error(error, stackTrace)), 256 | ); 257 | autoDisposeCustom(subscriber.cancel); 258 | } 259 | 260 | /// Subscribes to the data output of this [Node] permanently. 261 | /// 262 | /// This method works similarly to [subscribePermanently], except that errors 263 | /// are simply ignored. 264 | void subscribeToDataPermanently(void Function(T data) callback) { 265 | _ensureWired(); 266 | final subscriber = _stream!.listen( 267 | callback, 268 | onError: (error, stackTrace) {}, 269 | ); 270 | autoDisposeCustom(subscriber.cancel); 271 | } 272 | 273 | /// Modifier function: instructs the [Node] to be settable. 274 | /// 275 | /// Requires that this [Node] is in either the `ReadToConfigure` or the 276 | /// `ReadyToWire` state. See the `Lifecycle of a [Node]` section of the class 277 | /// documentation. 278 | /// 279 | /// Common usage: 280 | /// 281 | /// ```dart 282 | /// final myNode = Node(); 283 | /// 284 | /// myNode 285 | /// ..asSettable() 286 | /// ..wire0(); 287 | /// ``` 288 | /// You may also call [wire0] before [asSettable]. 289 | void asSettable() { 290 | checkDisposed(); 291 | checkState(_stream == null, message: '$this has been wired'); 292 | _settable = true; 293 | } 294 | 295 | /// Modifier function: sets the initial event to [event]. 296 | /// 297 | /// Requires that this [Node] is in either the `ReadToConfigure` or the 298 | /// `ReadyToWire` state. See the `Lifecycle of a [Node]` section of the class 299 | /// documentation. 300 | /// 301 | /// Common usage: 302 | /// 303 | /// ```dart 304 | /// final myNode = Node(); 305 | /// 306 | /// myNode 307 | /// ..withInitialEvent(StreamEvent.data('Hello')) 308 | /// ..wire0(); 309 | /// ``` 310 | /// You may also call [wire0] before [withInitialEvent]. 311 | void withInitialEvent(StreamEvent event) { 312 | checkDisposed(); 313 | checkState(_stream == null, message: '$this has been wired'); 314 | _initialEvent = event; 315 | } 316 | 317 | /// Sets the initial event to [data]. 318 | void withInitialData(T data) => withInitialEvent(StreamEvent.data(data)); 319 | 320 | /// Wires this [Node] such that events from [source] are passed through this 321 | /// to its output without processing. 322 | /// 323 | /// [source] is not listened to until this [Node] is used. 324 | void wireAsPassthrough(Stream source) { 325 | _ensureReadyToConfigure(); 326 | _wireFromSource(() => source); 327 | } 328 | 329 | /// Wires this [Node] such that events from [source] are passed through this 330 | /// to its output without processing. 331 | /// 332 | /// [source] is not accessed until this [Node] is used. 333 | void wireAsPassthroughNode(Node source) { 334 | _ensureReadyToConfigure(); 335 | _wireFromSource(() => source.stream); 336 | } 337 | 338 | /// Wires this [Node] such that events from [source] are transformed by 339 | /// [transformer] before emitted to its output stream. 340 | /// 341 | /// An alternative is to transform the source stream directly and pass the 342 | /// transformed stream to [wireAsPassthrough]: 343 | /// 344 | /// ```dart 345 | /// final stream = source.transform(debounce(Duration(milliseconds: 500))); 346 | /// final node = Node()..wireAsPassThrough(stream); 347 | /// ``` 348 | /// 349 | /// The disadvantage is: the debounce logic is evaluated immediately and data 350 | /// starts to flow as soon as they are emitted. 351 | /// 352 | /// With [wireAsTransformedStream], the wiring logic where the transformer is 353 | /// applied is evaluated lazily. So the debounce logic will not execute until 354 | /// there is an actual user of this [Node]. 355 | /// 356 | /// ```dart 357 | /// final node = Node() 358 | /// ..wireAsTransformedStream( 359 | /// source, 360 | /// debounce(Duration(milliseconds: 500))); 361 | /// ``` 362 | void wireAsTransformedStream( 363 | Stream source, 364 | StreamTransformer transformer, 365 | ) { 366 | _ensureReadyToConfigure(); 367 | _wireFromSource(() => source.transform(transformer)); 368 | } 369 | 370 | /// Wires this [Node] such that output events from [source] are transformed by 371 | /// [transformer] before emitted to its output stream. 372 | /// 373 | /// This is functionally the same as [wireAsTransformedStream], except that it 374 | /// takes a [Node] rather than a direct [Stream]. The alternative is to call 375 | /// [wireAsTransformedStream] directly: 376 | /// 377 | /// ```dart 378 | /// final node = Node() 379 | /// ..wireAsTransformedStream( 380 | /// sourceNode.stream, 381 | /// debounce(Duration(milliseconds: 500))); 382 | /// ``` 383 | /// 384 | /// However, [stream] getter is called on `sourceNode` immediately, resulting 385 | /// in the actual wiring of `sourceNode`. With [wireAsTransformedNode], the 386 | /// wiring of [sourceNode] is delayed until there is an actual user of this 387 | /// [Node]. Otherwise, these two approaches are equivalent. 388 | void wireAsTransformedNode( 389 | Node source, 390 | StreamTransformer transformer, 391 | ) { 392 | _ensureReadyToConfigure(); 393 | _wireFromSource(() => source.stream.transform(transformer)); 394 | } 395 | 396 | /// Wires this [Node] such that it has no dependency. 397 | /// 398 | /// Here are some examples with [wire0]: 399 | /// 400 | /// ```dart 401 | /// // A node with no value. 402 | /// final idleNode = Node..wire0(); 403 | /// 404 | /// // A node with a single value that does not change. 405 | /// final singleValueNode = Node..withInitialData(123)..wire0(); 406 | /// 407 | /// // A node that emits whatever is set. 408 | /// final settableNode = Node..asSettable()..wire0(); 409 | /// ``` 410 | void wire0() { 411 | _ensureReadyToConfigure(); 412 | _wireFromSource(() => Stream.empty()); 413 | } 414 | 415 | /// Wires this [Node] such that it has one input node. 416 | /// 417 | /// The latest value from the input is converted by [converter] and passed to 418 | /// this [Node]'s output. Errors are forwarded. 419 | void wire1(Node n1, FutureOr Function(E1) converter) { 420 | _ensureReadyToConfigure(); 421 | _wireFromSource(() => n1.stream.asyncMap(converter)); 422 | } 423 | 424 | /// Wires this [Node] such that it has two input nodes. 425 | /// 426 | /// Latest values from the inputs are converted by [converter] and passed to 427 | /// this [Node]'s output. Errors are forwarded. 428 | void wire2( 429 | Node n1, 430 | Node n2, 431 | FutureOr Function(E1, E2) converter, 432 | ) { 433 | _ensureReadyToConfigure(); 434 | _wireFromSource( 435 | () => combineLatest2(n1.stream, n2.stream, converter), 436 | ); 437 | } 438 | 439 | /// Wires this [Node] such that it has three input nodes. 440 | /// 441 | /// Latest values from the inputs are converted by [converter] and passed to 442 | /// this [Node]'s output. Errors are forwarded. 443 | void wire3( 444 | Node n1, 445 | Node n2, 446 | Node n3, 447 | FutureOr Function(E1, E2, E3) converter, 448 | ) { 449 | _ensureReadyToConfigure(); 450 | _wireFromSource( 451 | () => combineLatest3( 452 | n1.stream, 453 | n2.stream, 454 | n3.stream, 455 | converter, 456 | ), 457 | ); 458 | } 459 | 460 | /// Wires this [Node] such that it has four input nodes. 461 | /// 462 | /// Latest values from the inputs are converted by [converter] and passed to 463 | /// this [Node]'s output. Errors are forwarded. 464 | void wire4( 465 | Node n1, 466 | Node n2, 467 | Node n3, 468 | Node n4, 469 | FutureOr Function(E1, E2, E3, E4) converter, 470 | ) { 471 | _ensureReadyToConfigure(); 472 | _wireFromSource( 473 | () => combineLatest4( 474 | n1.stream, 475 | n2.stream, 476 | n3.stream, 477 | n4.stream, 478 | converter, 479 | ), 480 | ); 481 | } 482 | 483 | /// Wires this [Node] such that it has five input nodes. 484 | /// 485 | /// Latest values from the inputs are converted by [converter] and passed to 486 | /// this [Node]'s output. Errors are forwarded. 487 | void wire5( 488 | Node n1, 489 | Node n2, 490 | Node n3, 491 | Node n4, 492 | Node n5, 493 | FutureOr Function(E1, E2, E3, E4, E5) converter, 494 | ) { 495 | _ensureReadyToConfigure(); 496 | _wireFromSource( 497 | () => combineLatest5( 498 | n1.stream, 499 | n2.stream, 500 | n3.stream, 501 | n4.stream, 502 | n5.stream, 503 | converter, 504 | ), 505 | ); 506 | } 507 | 508 | /// Wires this [Node] such that it has six input nodes. 509 | /// 510 | /// Latest values from the inputs are converted by [converter] and passed to 511 | /// this [Node]'s output. Errors are forwarded. 512 | void wire6( 513 | Node n1, 514 | Node n2, 515 | Node n3, 516 | Node n4, 517 | Node n5, 518 | Node n6, 519 | FutureOr Function(E1, E2, E3, E4, E5, E6) converter, 520 | ) { 521 | _ensureReadyToConfigure(); 522 | _wireFromSource( 523 | () => combineLatest6( 524 | n1.stream, 525 | n2.stream, 526 | n3.stream, 527 | n4.stream, 528 | n5.stream, 529 | n6.stream, 530 | converter, 531 | ), 532 | ); 533 | } 534 | 535 | /// Wires this [Node] such that it has seven input nodes. 536 | /// 537 | /// Latest values from the inputs are converted by [converter] and passed to 538 | /// this [Node]'s output. Errors are forwarded. 539 | void wire7( 540 | Node n1, 541 | Node n2, 542 | Node n3, 543 | Node n4, 544 | Node n5, 545 | Node n6, 546 | Node n7, 547 | FutureOr Function(E1, E2, E3, E4, E5, E6, E7) converter, 548 | ) { 549 | _ensureReadyToConfigure(); 550 | _wireFromSource( 551 | () => combineLatest7( 552 | n1.stream, 553 | n2.stream, 554 | n3.stream, 555 | n4.stream, 556 | n5.stream, 557 | n6.stream, 558 | n7.stream, 559 | converter, 560 | ), 561 | ); 562 | } 563 | 564 | /// Ensures this [Node] is wired and not disposed of. 565 | /// 566 | /// This method calls [_wiringCallback] if this [Node] has been configured but 567 | /// not wired. 568 | void _ensureWired() { 569 | if (_stream != null) { 570 | return; 571 | } 572 | checkState( 573 | _wiringCallback != null, 574 | message: '$this has not been configured for wiring.', 575 | ); 576 | _wiringCallback!(); 577 | _wiringCallback = null; 578 | _initialEvent = null; 579 | checkState(_stream != null); 580 | } 581 | 582 | /// Ensures this [Node] is not wired, not disposed of, and not configured for 583 | /// wiring. 584 | void _ensureReadyToConfigure() { 585 | checkState(_stream == null, message: '$this has been wired'); 586 | checkState( 587 | _wiringCallback == null, 588 | message: '$this has been configured for wiring', 589 | ); 590 | } 591 | 592 | /// Sets [_wiringCallback] from the [Stream] returned by [sourceBuilder]. 593 | /// 594 | /// [sourceBuilder] is called within the [_wiringCallback] to ensure any 595 | /// action performed by [sourceBuilder] is done lazily. 596 | void _wireFromSource(Stream Function() sourceBuilder) { 597 | assert(_wiringCallback == null); 598 | _wiringCallback = () { 599 | final source = sourceBuilder(); 600 | if (_settable) { 601 | // A settable [Node] requires its own [StreamController] and is thus 602 | // slightly more expensive. 603 | final controller = StreamController( 604 | // Forwarding only. 605 | sync: true, 606 | ); 607 | autoDisposeCustom(controller.close); 608 | 609 | // Set up the forwarding of events from [source]. 610 | final listener = source.listen( 611 | controller.add, 612 | onError: controller.addError, 613 | ); 614 | autoDisposeCustom(listener.cancel); 615 | 616 | _stream = ReplayStream(controller.stream, initialEvent: _initialEvent); 617 | _setter = (StreamEvent event) => event.emitToController(controller); 618 | } else { 619 | _stream = ReplayStream(source, initialEvent: _initialEvent); 620 | } 621 | }; 622 | } 623 | 624 | @override 625 | String toString() { 626 | if (isDisposed) { 627 | return 'Node<$T>.disposed'; 628 | } 629 | if (_stream != null) { 630 | final settable = _setter == null ? '' : '.settable'; 631 | return 'Node<$T>.wired$settable'; 632 | } 633 | if (_wiringCallback == null) { 634 | return 'Node<$T>.constructed'; 635 | } 636 | return 'Node<$T>.configured'; 637 | } 638 | } 639 | -------------------------------------------------------------------------------- /lib/flow_converters.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Useful converters for the flow library. 16 | 17 | import 'dart:async'; 18 | 19 | import 'package:tuple/tuple.dart'; 20 | 21 | import 'stream_event.dart'; 22 | 23 | /// Converts a pair of values into a [Tuple2], useful for wiring a `Node` with 24 | /// two inputs. 25 | Tuple2 convertToTuple2(E1 v1, E2 v2) => Tuple2(v1, v2); 26 | 27 | /// Converts a triplet of values into a [Tuple3], useful for wiring a `Node` 28 | /// with three inputs. 29 | Tuple3 convertToTuple3(E1 v1, E2 v2, E3 v3) => 30 | Tuple3(v1, v2, v3); 31 | 32 | /// Converts a quadruplet of values into a [Tuple4], useful for wiring a `Node` 33 | /// with four inputs. 34 | Tuple4 convertToTuple4( 35 | E1 v1, 36 | E2 v2, 37 | E3 v3, 38 | E4 v4, 39 | ) => 40 | Tuple4(v1, v2, v3, v4); 41 | 42 | /// Converts a quintuple of values into a [Tuple5], useful for wiring a `Node` 43 | /// with five inputs. 44 | Tuple5 convertToTuple5( 45 | E1 v1, 46 | E2 v2, 47 | E3 v3, 48 | E4 v4, 49 | E5 v5, 50 | ) => 51 | Tuple5(v1, v2, v3, v4, v5); 52 | 53 | /// Converts a six-tuple of values into a [Tuple6], useful for wiring a `Node` 54 | /// with six inputs. 55 | Tuple6 convertToTuple6( 56 | E1 v1, 57 | E2 v2, 58 | E3 v3, 59 | E4 v4, 60 | E5 v5, 61 | E6 v6, 62 | ) => 63 | Tuple6(v1, v2, v3, v4, v5, v6); 64 | 65 | /// Converts a seven-tuple of values into a [Tuple7], useful for wiring a `Node` 66 | /// with seven inputs. 67 | Tuple7 convertToTuple7( 68 | E1 v1, 69 | E2 v2, 70 | E3 v3, 71 | E4 v4, 72 | E5 v5, 73 | E6 v6, 74 | E7 v7, 75 | ) => 76 | Tuple7(v1, v2, v3, v4, v5, v6, v7); 77 | 78 | /// A [StreamTransformer] that applies [converter] to each source [StreamEvent] 79 | /// and emits another [StreamEvent]. 80 | /// 81 | /// Unlike a regular converter function which only converts data values, this 82 | /// transformer accepts a converter function that takes both data values and 83 | /// errors. 84 | /// 85 | /// When [converter] returns a data [StreamEvent], the value is emitted. 86 | /// 87 | /// When [converter] returns an error [StreamEvent] or throws, the error is 88 | /// emitted. 89 | StreamTransformer convertStreamEvent( 90 | StreamEvent Function(StreamEvent) converter, 91 | ) => 92 | StreamTransformer.fromHandlers( 93 | handleData: (S data, EventSink sink) => 94 | _convertAndEmit(sink, StreamEvent.data(data), converter), 95 | handleError: (Object error, StackTrace stackTrace, EventSink sink) => 96 | _convertAndEmit( 97 | sink, 98 | StreamEvent.error(error, stackTrace), 99 | converter, 100 | ), 101 | handleDone: (EventSink sink) => sink.close(), 102 | ); 103 | 104 | void _convertAndEmit( 105 | EventSink sink, 106 | StreamEvent input, 107 | StreamEvent Function(StreamEvent) converter, 108 | ) { 109 | try { 110 | final output = converter(input); 111 | if (output.hasData) { 112 | sink.add(output.data); 113 | } else { 114 | sink.addError(output.error, output.stackTrace); 115 | } 116 | } catch (error, stackTrace) { 117 | sink.addError(error, stackTrace); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/ordered_executor.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// A library that supports executing the same async function multiple times in 16 | /// an orderly fashion. 17 | 18 | import 'dart:async'; 19 | import 'dart:collection'; 20 | 21 | import 'package:auto_disposable/auto_disposable.dart'; 22 | import 'package:quiver/check.dart'; 23 | 24 | import 'flow.dart'; 25 | 26 | /// Final status of an individual execution. 27 | enum ExecutionStatus { 28 | /// This execution has completed and its result, data or error, was emitted. 29 | done, 30 | 31 | /// This execution was preempted by a later execution started before this 32 | /// execution was able to complete. As a result, its result was simply not 33 | /// emitted. 34 | preempted, 35 | 36 | /// This execution was aborted because it did not complete before this 37 | /// [OrderedExecutor] was disposed. 38 | aborted, 39 | } 40 | 41 | /// An executor that supports running the same async function multiple times 42 | /// with different params. 43 | /// 44 | /// This executor enforces the ordering of those executions. While multiple 45 | /// execution may occur, only the last one may emit value/error and others are 46 | /// simply preempted and their results ignored. 47 | /// 48 | /// If a new execution is scheduled by [run()] when there is already a pending 49 | /// execution with the same [Param], this executor does not start a new 50 | /// execution. This is useful to avoid running duplicate async functions that 51 | /// are expensive. 52 | /// 53 | /// See [ExecutionStatus] for supported execution outcomes. 54 | /// 55 | /// # When to use 56 | /// 57 | /// This class is best suited if you have an existing data flow that depends on 58 | /// some async function to update. See `flow.dart`. 59 | /// 60 | /// Particularly, this is useful when updates are expensive and dependent on 61 | /// user interactions. For example, a user can potentially attempt to refresh 62 | /// some data a few times within a short amount of time. And each refresh 63 | /// function involves an expensive RPC. If an RPC is in-flight, you can make use 64 | /// of an [OrderedExecutor] to avoid sending a duplicate one. 65 | /// 66 | /// # Side effects 67 | /// 68 | /// Note you cannot terminate an async function. So any side effect in your 69 | /// callback remains effective. For example, if your callback updates a cache, 70 | /// the ordering of those cache updates is undefined: 71 | /// 72 | /// ```dart 73 | /// Future fetchBooks(String bookType) async { 74 | /// final List books = await expensiveRpc(bookType); 75 | /// _cache.update(bookType, books); 76 | /// return true; 77 | /// } 78 | /// ``` 79 | /// In this example, [fetchBooks] has a side effect - it updates the cache 80 | /// inside the function. If multiple [fetchBooks] are running, the cache updates 81 | /// happen in an undefined order. It is generally recommended that you remove 82 | /// side effects from the callback function: 83 | /// 84 | /// ```dart 85 | /// /// Might be better to replace [Tuple2] with your own data structure. 86 | /// Future>> fetchBooks(String bookType) async { 87 | /// final books = await expensiveRpc(bookType); 88 | /// return Tuple2(bookType, books); 89 | /// } 90 | /// 91 | /// final executor = OrderedExecutor(fetchBooks); 92 | /// 93 | /// executor.result.subscribeToDataPermanently((tuple2) { 94 | /// _cache.update(tuple2.item1, tuple2.item2); 95 | /// }); 96 | /// ``` 97 | class OrderedExecutor with AutoDisposerMixin { 98 | /// Constructs an [OrderedExecutor] with the async function to run. 99 | /// 100 | /// The async function is allowed to return `null`s or throw. The result, 101 | /// either data or error, is emitted to [result] if and only if the execution 102 | /// returns [ExecutionStatus.done]. See [run()] for details. 103 | OrderedExecutor(this._callback) { 104 | result 105 | ..asSettable() 106 | ..wire0(); 107 | autoDispose(result); 108 | 109 | busy 110 | ..withInitialData(false) 111 | ..asSettable() 112 | ..wire0(); 113 | autoDispose(busy); 114 | } 115 | 116 | /// Async function to run by this executor. 117 | final Future Function(Param) _callback; 118 | 119 | /// Result [Node] of the executions. 120 | final result = Node(); 121 | 122 | /// Status [Node] of whether this executor is busy running. 123 | final busy = Node(); 124 | 125 | /// Pending executions. 126 | final _pending = HashMap>(); 127 | 128 | /// [Param] of the latest execution. 129 | Param? _latest; 130 | 131 | /// Starts an execution with [param] and returns a [Future] with the final 132 | /// status of this execution. The returned [Future] never throws. 133 | /// 134 | /// See [ExecutionStatus] for when a specific value is returned. 135 | /// 136 | /// If there is a pending execution with the same [param], this method does 137 | /// not start a new execution. It simply returns a [Future] that binds to the 138 | /// same pending execution. 139 | Future run(Param param) async { 140 | checkDisposed(); 141 | checkArgument( 142 | param != null, 143 | message: 'Param is required', 144 | ); 145 | // Update [_latest] immediately as it determines which execution should emit 146 | // when it is complete. 147 | _latest = param; 148 | final matchingExecution = _pending[param]; 149 | if (matchingExecution != null) { 150 | // Return immediately when there is a matching execution, without starting 151 | // a new one. 152 | return matchingExecution.future; 153 | } 154 | return _startExecution(param); 155 | } 156 | 157 | /// Starts a new execution with non-null [param] assuming there is no pending 158 | /// execution with the same [param]. 159 | Future _startExecution(Param param) { 160 | assert(param != null); 161 | if (_pending.isEmpty) { 162 | busy.value = true; 163 | } 164 | final completer = Completer(); 165 | _pending[param] = completer; 166 | _runGuarded(param); 167 | return completer.future; 168 | } 169 | 170 | void _runGuarded(Param param) async { 171 | try { 172 | _completeWithResult(param, await _callback(param)); 173 | } catch (error) { 174 | _completeWithError(param, error); 175 | } 176 | } 177 | 178 | void _completeWithResult(Param param, Result value) { 179 | final completer = _complete(param); 180 | if (completer != null) { 181 | result.value = value; 182 | completer.complete(ExecutionStatus.done); 183 | } 184 | } 185 | 186 | void _completeWithError(Param param, Object error) { 187 | final completer = _complete(param); 188 | if (completer != null) { 189 | result.error = error; 190 | completer.complete(ExecutionStatus.done); 191 | } 192 | } 193 | 194 | Completer? _complete(Param param) { 195 | final completer = _pending.remove(param); 196 | if (completer == null) { 197 | return null; 198 | } 199 | if (isDisposed) { 200 | completer.complete(ExecutionStatus.aborted); 201 | return null; 202 | } 203 | if (_pending.isEmpty) { 204 | busy.value = false; 205 | } 206 | if (param != _latest) { 207 | completer.complete(ExecutionStatus.preempted); 208 | return null; 209 | } 210 | return completer; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /lib/replay_stream.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | import 'dart:collection'; 17 | 18 | import 'package:meta/meta.dart'; 19 | 20 | import 'stream_event.dart'; 21 | 22 | /// A [Stream] implementation that replays the latest event. 23 | /// 24 | /// A [ReplayStream] is always a broadcast stream. [ReplayStream.isBroadcast] 25 | /// always returns `true`. All types of events can be replayed, including data 26 | /// and errors. 27 | /// 28 | /// # Construction 29 | /// 30 | /// A [ReplayStream] can be created from an existing [Stream]: 31 | /// 32 | /// ```dart 33 | /// final replayStream = ReplayStream(myStream); 34 | /// ``` 35 | /// 36 | /// [ReplayStream]s created this way do not have an event to replay until the 37 | /// delegate (`myStream` in the example) notifies it of some event. To ensure 38 | /// that an event is always available to replay, you may pass an `initialEvent` 39 | /// to the constructor: 40 | /// 41 | /// ```dart 42 | /// final replayStream = ReplayStream( 43 | /// myStream, 44 | /// initialEvent: StreamEvent.data(123), 45 | /// ); 46 | /// ``` 47 | /// 48 | /// # Contract 49 | /// 50 | /// By default, the delegate [Stream] is subscribed to once this [ReplayStream] 51 | /// is constructed. 52 | /// 53 | /// If [idleUntilFirstSubscription] is `true`, however, the delegate [Stream] is 54 | /// only subscribed to when this [ReplayStream] has its first subscriber. 55 | /// 56 | /// Note that [ReplayStream] does not have time-travel abilities. Only the 57 | /// latest event emitted from the delegate [Stream] AFTER this [ReplayStream] 58 | /// subscribes to the former can be replayed. 59 | /// 60 | /// The latest event can be retrieved synchronously using the [event] property: 61 | /// 62 | /// ```dart 63 | /// final event = replayStream.event; 64 | /// ``` 65 | /// 66 | /// Once a [ReplayStream] subscribes to the delegate [Stream], it keeps the 67 | /// subscription active until the delegate [Stream] is closed. There is no way 68 | /// to cancel the subscription prematurely. 69 | /// 70 | /// # Closed streams 71 | /// 72 | /// If the delegate [Stream] closes, this [ReplayStream] forwards the `done` 73 | /// event to current subscribers and closes them. Future subscribers will 74 | /// receive the latest event if available, followed immediately by a `done` 75 | /// event. 76 | /// 77 | /// Even if a [ReplayStream] is closed, its `event` property is still 78 | /// accessible. It contains the latest event received before the closure. 79 | /// 80 | /// # Idle until first subscription 81 | /// 82 | /// A common pattern around streams is to start generating events only after 83 | /// the stream has been subscribed to. For example: 84 | /// 85 | /// ```dart 86 | /// final controller = StreamController(onListen: _generateData); 87 | /// Stream get stream => controller.stream; 88 | /// 89 | /// ... 90 | /// 91 | /// stream.listen(...); 92 | /// 93 | /// ``` 94 | /// 95 | /// `_generateData` is only called after the stream is subscribed to so it can 96 | /// start adding data to the controller. 97 | /// 98 | /// This pattern does not work with the default [ReplayStream], because 99 | /// [ReplayStream] needs to subscribe to the delegate [Stream] to get the latest 100 | /// event recorded. 101 | /// 102 | /// If this behavior is undesired, you may pass `true` to the 103 | /// [idleUntilFirstSubscription] constructor argument. As a result, 104 | /// [ReplayStream] stays docile and does not listen to the delegate stream. When 105 | /// this [ReplayStream] is first subscribed to, it will start listening to the 106 | /// delegate stream and recording the latest event. 107 | /// 108 | /// # Callback ordering 109 | /// 110 | /// When a [ReplayStream] has multiple subscribers, the order in which they are 111 | /// notified of a new event is undefined but stable. For details, see the 112 | /// iteration order of [HashSet]. 113 | class ReplayStream extends Stream { 114 | /// Creates a [ReplayStream] from [_stream] that replays the latest event from 115 | /// [_stream] to new subscribers. 116 | /// 117 | /// If [initialEvent] is not null, it is replayed to new subscribers until 118 | /// there is an event from [_stream]. 119 | /// 120 | /// If [idleUntilFirstSubscription] is true, this [ReplayStream] does not 121 | /// listen to [_stream] until it has its first subscriber. Note if [_stream] 122 | /// is a hot stream and has other subscribers, any event emitted will be lost 123 | /// until this [ReplayStream]'s first subscription. 124 | /// 125 | /// If [idleUntilFirstSubscription] is false (default behavior), this 126 | /// [ReplayStream] starts listening to [_stream] immediately. 127 | ReplayStream( 128 | this._stream, { 129 | StreamEvent? initialEvent, 130 | bool idleUntilFirstSubscription = false, 131 | }) : _event = initialEvent { 132 | if (!idleUntilFirstSubscription) { 133 | _ensureConfigured(); 134 | } 135 | } 136 | 137 | /// A [ReplayStream] is always broadcast. 138 | @override 139 | bool get isBroadcast => true; 140 | 141 | /// Returns the latest event emitted from the stream, if available. 142 | /// 143 | /// [event] is guaranteed to be updated with a value `X` before any 144 | /// subscriber of this [ReplayStream] learn of this value. 145 | StreamEvent? get event => _event; 146 | 147 | /// Whether this [ReplayStream] has been configured. 148 | bool _configured = false; 149 | 150 | /// The delegate stream from the constructor argument. 151 | final Stream _stream; 152 | 153 | /// Subscription on the delegate stream. 154 | StreamSubscription? _listener; 155 | 156 | /// The latest value of the delegate stream. 157 | StreamEvent? _event; 158 | 159 | /// Current subscribers. 160 | /// 161 | /// Each subscriber is identified by its private [StreamController] object. 162 | /// [_subscribers] is `null` when [_configured] is `false`. It is also set to 163 | /// `null` when the delegate stream is closed. 164 | Set>? _subscribers; 165 | 166 | /// Returns true if this [ReplayStream] has seen the `done` event. 167 | bool get _isClosed => _configured && _subscribers == null; 168 | 169 | /// Configures this [ReplayStream] so it starts to record the latest event. 170 | /// 171 | /// This method is idempotent. 172 | void _ensureConfigured() { 173 | if (_configured) { 174 | return; 175 | } 176 | _configured = true; 177 | _subscribers = HashSet>(); 178 | _listener = _stream.listen(_onData, onError: _onError, onDone: _onDone); 179 | } 180 | 181 | /// Updates [event] and forwards [data] to current subscribers. 182 | void _onData(T data) { 183 | _event = StreamEvent.data(data); 184 | _forwardEvent(_event!); 185 | } 186 | 187 | /// Updates [event] and forwards [error] and [stackTrace] to current 188 | /// subscribers. 189 | void _onError(Object error, StackTrace stackTrace) { 190 | _event = StreamEvent.error(error, stackTrace); 191 | _forwardEvent(_event!); 192 | } 193 | 194 | /// Cancels [_listener] on the delegate stream and closes all existing 195 | /// subscribers. 196 | /// 197 | /// After this call, [_subscribers] is `null`. 198 | void _onDone() { 199 | if (_listener != null) { 200 | _listener!.cancel(); 201 | _listener = null; 202 | } 203 | if (_subscribers != null) { 204 | final subscribers = _subscribers!.toList(growable: false); 205 | _subscribers = null; 206 | for (final controller in subscribers) { 207 | // Close the [controller] to send a `done` event. 208 | controller.close(); 209 | } 210 | } 211 | } 212 | 213 | void dispose() { 214 | _onDone(); 215 | } 216 | 217 | /// Forwards [event] to current subscribers. 218 | /// 219 | /// A defensive copy of [_subscriber] is made immediately in case of 220 | /// subscriptions/unsubscriptions during this call. A subscriber added in one 221 | /// of the callbacks will not get this [event]. A subscriber removed in one of 222 | /// the callbacks may or may not get this [event] depending on the order of 223 | /// the callbacks. 224 | void _forwardEvent(StreamEvent event) { 225 | final subscribers = _subscribers!.toList(growable: false); 226 | for (final controller in subscribers) { 227 | if (_subscribers!.contains(controller)) { 228 | event.emitToController(controller); 229 | } 230 | } 231 | } 232 | 233 | /// Listens to the [ReplayStream]. If a latest [event] is available, it is 234 | /// passed to the listener as if it is the first event on the stream. 235 | /// 236 | /// For synchronous access to the latest event, use the [event] getter 237 | /// directly. 238 | @override 239 | StreamSubscription listen( 240 | void Function(T data)? onData, { 241 | Function? onError, 242 | void Function()? onDone, 243 | bool? cancelOnError, 244 | }) { 245 | _ensureConfigured(); 246 | StreamController? controller; 247 | controller = StreamController( 248 | // Forwarding only. 249 | sync: true, 250 | onCancel: () { 251 | _unsubscribe(controller); 252 | }, 253 | ); 254 | _replayEvent(controller); 255 | if (_isClosed) { 256 | controller.close(); 257 | } else { 258 | _subscribe(controller); 259 | } 260 | return controller.stream.listen( 261 | onData, 262 | onError: onError, 263 | onDone: onDone, 264 | cancelOnError: cancelOnError, 265 | ); 266 | } 267 | 268 | /// Emits [_event] to [controller], if [_event] is not `null`. 269 | void _replayEvent(StreamController controller) => 270 | _event?.emitToController(controller); 271 | 272 | /// Adds a new subscriber identified by [controller]. 273 | void _subscribe(StreamController controller) { 274 | assert(_subscribers != null); 275 | _subscribers!.add(controller); 276 | } 277 | 278 | /// Removes an existing subscriber identified by [controller]. 279 | /// 280 | /// This method does nothing if an existing subscriber cannot be found. Note 281 | /// there is no need to close the [controller] as there is no cleanup other 282 | /// than removing it from the [_subscribers] set. 283 | void _unsubscribe(StreamController? controller) => 284 | _subscribers?.remove(controller); 285 | 286 | /// Returns true if this stream is listening to the stream it's replaying. 287 | /// 288 | /// This becomes true when the stream being replayed starts being listened to 289 | /// and becomes false when the stream being listened to is done or this 290 | /// stream is disposed. 291 | @visibleForTesting 292 | bool get isListening => _listener != null; 293 | } 294 | -------------------------------------------------------------------------------- /lib/sequential_executor.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// A library that supports executing different async functions in sequential 16 | /// order. 17 | 18 | import 'dart:collection'; 19 | 20 | import 'package:quiver/check.dart'; 21 | 22 | /// A sequencer of asynchronous tasks. 23 | /// 24 | /// If there is no pending task, a newly added task starts executing at a later 25 | /// microtask but as soon as Dart allows. The small gap is to allow the caller 26 | /// attach error catchers. 27 | /// 28 | /// If there are pending tasks, a newly added task waits until all of them are 29 | /// completed before it starts to execute. Even if some tasks threw, the new 30 | /// task is not affected and continues to execute. 31 | /// 32 | /// # Zones 33 | /// 34 | /// [SequentialExecutor] does not have any special handling of Dart zones. Apps 35 | /// should execute the usual caution in a multi-zone environment, especially 36 | /// around error-handling. See 37 | /// https://dart.dev/articles/archive/zones#handling-asynchronous-errors for 38 | /// details. 39 | class SequentialExecutor { 40 | /// Queued tasks. 41 | final _queue = Queue(); 42 | 43 | /// Submits a new [task] into the internal queue and returns a [Future] that 44 | /// completes with [task]'s result. 45 | /// 46 | /// [task] is not run until pending tasks added earlier are completed. 47 | Future run(Future Function() task) { 48 | checkArgument(task != null); 49 | if (_queue.isEmpty) { 50 | return _enqueue(_run(task)); 51 | } 52 | // Wait for the last queued task to finish, then execute this [task]. 53 | return _enqueue(_chain(_queue.last, task)); 54 | } 55 | 56 | /// Enqueues [future] and make it remove itself from the queue when completed. 57 | Future _enqueue(Future future) { 58 | _queue.add(future); 59 | return future.whenComplete(_queue.removeFirst); 60 | } 61 | 62 | /// Runs [task] at a later microtask to ensure the caller has a chance to 63 | /// attach error catchers. 64 | Future _run(Future Function() task) async => await task(); 65 | 66 | /// Runs [task] after [future] is done and returns the task's result. 67 | Future _chain(Future future, Future Function() task) async { 68 | try { 69 | await future; 70 | } catch (e) { 71 | // Intentionally catch all. The previous task's status doesn't matter. 72 | } 73 | return await task(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/stream_event.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | import 'dart:math'; 17 | 18 | import 'package:meta/meta.dart'; 19 | import 'package:quiver/check.dart'; 20 | import 'package:quiver/core.dart'; 21 | 22 | /// One of the two possible states of [StreamEvent]. They are mutually 23 | /// exclusive. 24 | enum _StreamEventState { 25 | hasData, 26 | hasError, 27 | } 28 | 29 | /// An immutable class representing an event from a [Stream]. 30 | @immutable 31 | class StreamEvent { 32 | /// State of this [StreamEvent]. 33 | final _StreamEventState _state; 34 | 35 | /// Whether this event contains `data`. 36 | /// 37 | /// [hasData] is always the negate of [hasError]. 38 | bool get hasData => _state == _StreamEventState.hasData; 39 | 40 | /// Whether this event contains `error`. 41 | /// 42 | /// [hasData] is always the negate of [hasError]. 43 | bool get hasError => _state == _StreamEventState.hasError; 44 | 45 | /// Data of this event. 46 | /// 47 | /// Access to this getter is prohibited when [hasData] is `false`. This field 48 | /// can be `null`. 49 | T? get data { 50 | checkState(hasData); 51 | return _data; 52 | } 53 | 54 | final T? _data; 55 | 56 | /// Error of this event. 57 | /// 58 | /// Access to this getter is prohibited when [hasError] is `false`. 59 | Object get error { 60 | checkState(hasError); 61 | return _error!; 62 | } 63 | 64 | final Object? _error; 65 | 66 | /// StackTrace of this event. 67 | /// 68 | /// Access to this getter is prohibited when [hasError] is `false`. This field 69 | /// can be `null`. 70 | StackTrace? get stackTrace { 71 | checkState(hasError); 72 | return _stackTrace; 73 | } 74 | 75 | final StackTrace? _stackTrace; 76 | 77 | /// Constructs a [StreamEvent] with data and no error. 78 | const StreamEvent.data(T data) 79 | : _state = _StreamEventState.hasData, 80 | _data = data, 81 | _error = null, 82 | _stackTrace = null; 83 | 84 | /// Constructs a [StreamEvent] with error and no data. 85 | const StreamEvent.error( 86 | Object error, [ 87 | StackTrace stackTrace = StackTrace.empty, 88 | ]) : _state = _StreamEventState.hasError, 89 | _data = null, 90 | _error = error, 91 | _stackTrace = stackTrace; 92 | 93 | /// Emits this event to the given [controller]. 94 | void emitToController(StreamController controller) { 95 | if (hasData) { 96 | controller.add(data); 97 | } else { 98 | controller.addError(error, stackTrace); 99 | } 100 | } 101 | 102 | @override 103 | bool operator ==(other) { 104 | if (other is StreamEvent) { 105 | return other._state == _state && 106 | other._data == _data && 107 | other._error == _error && 108 | other._stackTrace == _stackTrace; 109 | } 110 | return false; 111 | } 112 | 113 | @override 114 | int get hashCode => hashObjects([ 115 | _state, 116 | _data, 117 | _error, 118 | _stackTrace, 119 | ]); 120 | 121 | @override 122 | String toString() { 123 | var stackTraceString = _stackTrace?.toString() ?? ''; 124 | return '$runtimeType($_state, $_data, $_error' 125 | ', ${stackTraceString.substring(0, min(10, stackTraceString.length))})'; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/widgets.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// A library with classes that provide Flutter support. 16 | 17 | import 'dart:async'; 18 | 19 | import 'package:flutter/widgets.dart'; 20 | 21 | /// A widget that builds itself based on the latest DISTINCT value from a 22 | /// [Stream]. 23 | /// 24 | /// This widget is more restrictive than [StreamBuilder] in the following ways: 25 | /// - [DistinctStreamBuilder] expects that the [Stream] does not throw errors. 26 | /// - If a new value from the [Stream] is the same as the previous value, 27 | /// nothing happens. 28 | /// 29 | /// # Configuration change. 30 | /// 31 | /// This widget resets the current value to [initialValue] and builds the next 32 | /// frame using [builder]. The current stream subscription is retained if 33 | /// [stream] is not changed. Otherwise, it is canceled and a new stream 34 | /// subscription created from the new [stream]. 35 | /// 36 | /// The only exception is when both [initialValue] and [stream] remain the same 37 | /// - this widget retains the current value and stream subscription. It builds 38 | /// the next frame with the current value and updated [builder]. 39 | class DistinctStreamBuilder extends StatefulWidget { 40 | const DistinctStreamBuilder({ 41 | required this.stream, 42 | required this.builder, 43 | this.initialValue, 44 | this.child, 45 | Key? key, 46 | }) : super(key: key); 47 | 48 | final Stream stream; 49 | final Widget Function(BuildContext, Widget?, T?) builder; 50 | final T? initialValue; 51 | final Widget? child; 52 | 53 | @override 54 | _DistinctStreamBuilderState createState() => _DistinctStreamBuilderState(); 55 | } 56 | 57 | /// [State] created by [DistinctStreamBuilder]. 58 | class _DistinctStreamBuilderState extends State> { 59 | T? _value; 60 | StreamSubscription? _listener; 61 | 62 | @override 63 | void initState() { 64 | super.initState(); 65 | _value = widget.initialValue; 66 | _subscribe(); 67 | } 68 | 69 | @override 70 | void didUpdateWidget(DistinctStreamBuilder oldWidget) { 71 | super.didUpdateWidget(oldWidget); 72 | if (oldWidget.stream == widget.stream && 73 | oldWidget.initialValue == widget.initialValue) { 74 | return; 75 | } 76 | _value = widget.initialValue; 77 | if (oldWidget.stream != widget.stream) { 78 | _unsubscribe(); 79 | _subscribe(); 80 | } 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) => 85 | widget.builder(context, widget.child, _value); 86 | 87 | @override 88 | void dispose() { 89 | _unsubscribe(); 90 | super.dispose(); 91 | } 92 | 93 | void _subscribe() { 94 | assert(_listener == null); 95 | _listener = widget.stream.listen((value) { 96 | if (value != _value) { 97 | setState(() => _value = value); 98 | } 99 | }); 100 | } 101 | 102 | void _unsubscribe() { 103 | if (_listener != null) { 104 | _listener!.cancel(); 105 | _listener = null; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: flutter_stream_extensions 16 | description: >- 17 | Utility package that makes stream-based programming in Flutter easier to reason about. 18 | version: 0.0.1 19 | homepage: https://github.com/google/flutter_stream_extensions 20 | 21 | environment: 22 | sdk: '>=2.17.6 <3.0.0' 23 | 24 | dependencies: 25 | flutter: 26 | sdk: flutter 27 | auto_disposable: 28 | git: 29 | url: https://github.com/google/auto-disposable.git 30 | meta: ^1.8.0 31 | quiver: ^3.1.0 32 | stream_transform: ^2.0.1 33 | tuple: ^2.0.1 34 | 35 | dev_dependencies: 36 | build_runner: ^2.3.0 37 | flutter_test: 38 | sdk: flutter 39 | mockito: ^5.3.2 40 | test: ^1.21.4 41 | -------------------------------------------------------------------------------- /test/combiners_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:flutter_stream_extensions/combiners.dart'; 18 | import 'package:test/test.dart'; 19 | import 'package:tuple/tuple.dart'; 20 | 21 | void main() { 22 | group('combineLatestTuple2', () { 23 | test('emits nothing when both inputs streams are empty', () { 24 | final stream = 25 | combineLatestTuple2(Stream.empty(), Stream.empty()); 26 | expect(stream, emits(emitsDone)); 27 | }); 28 | 29 | test('emits nothing when first input stream is empty', () { 30 | final stream = combineLatestTuple2( 31 | Stream.empty(), 32 | Stream.fromIterable([1, 2, 3]), 33 | ); 34 | expect(stream, emits(emitsDone)); 35 | }); 36 | 37 | test('emits nothing when second input stream is empty', () { 38 | final stream = combineLatestTuple2( 39 | Stream.fromIterable([1, 2, 3]), 40 | Stream.empty(), 41 | ); 42 | expect(stream, emits(emitsDone)); 43 | }); 44 | 45 | test('emits combined latest values from inputs', () { 46 | final controller1 = StreamController(); 47 | final controller2 = StreamController(); 48 | final stream = 49 | combineLatestTuple2(controller1.stream, controller2.stream); 50 | controller1.add(1); 51 | controller1.add(2); 52 | controller2.add(100); 53 | controller2.add(1000); 54 | controller1.close(); 55 | controller2.close(); 56 | expect( 57 | stream, 58 | emitsInOrder([ 59 | Tuple2(1, 100), 60 | Tuple2(2, 100), 61 | Tuple2(2, 1000), 62 | emitsDone, 63 | ]), 64 | ); 65 | }); 66 | }); 67 | 68 | group('combineLatestTuple3', () { 69 | test('emits nothing when all input streams are empty', () { 70 | final stream = combineLatestTuple3( 71 | Stream.empty(), 72 | Stream.empty(), 73 | Stream.empty(), 74 | ); 75 | expect(stream, emits(emitsDone)); 76 | }); 77 | 78 | test('emits nothing when first input stream is empty', () { 79 | final stream = combineLatestTuple3( 80 | Stream.empty(), 81 | Stream.fromIterable([1, 2, 3]), 82 | Stream.fromIterable([100, 1000]), 83 | ); 84 | expect(stream, emits(emitsDone)); 85 | }); 86 | 87 | test('emits nothing when second input stream is empty', () { 88 | final stream = combineLatestTuple3( 89 | Stream.fromIterable([1, 2, 3]), 90 | Stream.empty(), 91 | Stream.fromIterable([100, 1000]), 92 | ); 93 | expect(stream, emits(emitsDone)); 94 | }); 95 | 96 | test('emits nothing when third input stream is empty', () { 97 | final stream = combineLatestTuple3( 98 | Stream.fromIterable([1, 2, 3]), 99 | Stream.fromIterable([100, 1000]), 100 | Stream.empty(), 101 | ); 102 | expect(stream, emits(emitsDone)); 103 | }); 104 | 105 | test('emits combined latest values from inputs', () { 106 | final controller1 = StreamController(); 107 | final controller2 = StreamController(); 108 | final controller3 = StreamController(); 109 | final stream = combineLatestTuple3( 110 | controller1.stream, 111 | controller2.stream, 112 | controller3.stream, 113 | ); 114 | controller1.add(1); 115 | controller1.add(2); 116 | controller2.add(100); 117 | controller2.add(1000); 118 | controller3.add(10000); 119 | controller1.close(); 120 | controller2.close(); 121 | controller3.close(); 122 | expect( 123 | stream, 124 | emitsInOrder([ 125 | Tuple3(1, 100, 10000), 126 | Tuple3(2, 100, 10000), 127 | Tuple3(2, 1000, 10000), 128 | emitsDone, 129 | ]), 130 | ); 131 | }); 132 | }); 133 | 134 | group('combineLatestTuple4', () { 135 | test('emits nothing when all input streams are empty', () { 136 | final stream = combineLatestTuple4( 137 | Stream.empty(), 138 | Stream.empty(), 139 | Stream.empty(), 140 | Stream.empty(), 141 | ); 142 | expect(stream, emits(emitsDone)); 143 | }); 144 | 145 | test('emits nothing when first input stream is empty', () { 146 | final stream = combineLatestTuple4( 147 | Stream.empty(), 148 | Stream.fromIterable([1, 2, 3]), 149 | Stream.fromIterable([100, 1000]), 150 | Stream.fromIterable([10000]), 151 | ); 152 | expect(stream, emits(emitsDone)); 153 | }); 154 | 155 | test('emits nothing when second input stream is empty', () { 156 | final stream = combineLatestTuple4( 157 | Stream.fromIterable([1, 2, 3]), 158 | Stream.empty(), 159 | Stream.fromIterable([100, 1000]), 160 | Stream.fromIterable([10000]), 161 | ); 162 | expect(stream, emits(emitsDone)); 163 | }); 164 | 165 | test('emits nothing when third input stream is empty', () { 166 | final stream = combineLatestTuple4( 167 | Stream.fromIterable([1, 2, 3]), 168 | Stream.fromIterable([100, 1000]), 169 | Stream.empty(), 170 | Stream.fromIterable([10000]), 171 | ); 172 | expect(stream, emits(emitsDone)); 173 | }); 174 | 175 | test('emits nothing when fourth input stream is empty', () { 176 | final stream = combineLatestTuple4( 177 | Stream.fromIterable([1, 2, 3]), 178 | Stream.fromIterable([100, 1000]), 179 | Stream.fromIterable([10000]), 180 | Stream.empty(), 181 | ); 182 | expect(stream, emits(emitsDone)); 183 | }); 184 | 185 | test('emits combined latest values from inputs', () { 186 | final controller1 = StreamController(); 187 | final controller2 = StreamController(); 188 | final controller3 = StreamController(); 189 | final controller4 = StreamController(); 190 | final stream = combineLatestTuple4( 191 | controller1.stream, 192 | controller2.stream, 193 | controller3.stream, 194 | controller4.stream, 195 | ); 196 | controller1.add(1); 197 | controller1.add(2); 198 | controller2.add(100); 199 | controller2.add(1000); 200 | controller3.add(10000); 201 | controller4.add(100000); 202 | controller1.close(); 203 | controller2.close(); 204 | controller3.close(); 205 | controller4.close(); 206 | expect( 207 | stream, 208 | emitsInOrder([ 209 | Tuple4(1, 100, 10000, 100000), 210 | Tuple4(2, 100, 10000, 100000), 211 | Tuple4(2, 1000, 10000, 100000), 212 | emitsDone, 213 | ]), 214 | ); 215 | }); 216 | }); 217 | 218 | group('combineLatestTuple5', () { 219 | test('emits nothing when all input streams are empty', () { 220 | final stream = combineLatestTuple5( 221 | Stream.empty(), 222 | Stream.empty(), 223 | Stream.empty(), 224 | Stream.empty(), 225 | Stream.empty(), 226 | ); 227 | expect(stream, emits(emitsDone)); 228 | }); 229 | 230 | test('emits nothing when first input stream is empty', () { 231 | final stream = combineLatestTuple5( 232 | Stream.empty(), 233 | Stream.fromIterable([1, 2, 3]), 234 | Stream.fromIterable([100, 1000]), 235 | Stream.fromIterable([10000]), 236 | Stream.fromIterable([100000]), 237 | ); 238 | expect(stream, emits(emitsDone)); 239 | }); 240 | 241 | test('emits nothing when second input stream is empty', () { 242 | final stream = combineLatestTuple5( 243 | Stream.fromIterable([1, 2, 3]), 244 | Stream.empty(), 245 | Stream.fromIterable([100, 1000]), 246 | Stream.fromIterable([10000]), 247 | Stream.fromIterable([100000]), 248 | ); 249 | expect(stream, emits(emitsDone)); 250 | }); 251 | 252 | test('emits nothing when third input stream is empty', () { 253 | final stream = combineLatestTuple5( 254 | Stream.fromIterable([1, 2, 3]), 255 | Stream.fromIterable([100, 1000]), 256 | Stream.empty(), 257 | Stream.fromIterable([10000]), 258 | Stream.fromIterable([100000]), 259 | ); 260 | expect(stream, emits(emitsDone)); 261 | }); 262 | 263 | test('emits nothing when fourth input stream is empty', () { 264 | final stream = combineLatestTuple5( 265 | Stream.fromIterable([1, 2, 3]), 266 | Stream.fromIterable([100, 1000]), 267 | Stream.fromIterable([10000]), 268 | Stream.empty(), 269 | Stream.fromIterable([100000]), 270 | ); 271 | expect(stream, emits(emitsDone)); 272 | }); 273 | 274 | test('emits nothing when fifth input stream is empty', () { 275 | final stream = combineLatestTuple5( 276 | Stream.fromIterable([1, 2, 3]), 277 | Stream.fromIterable([100, 1000]), 278 | Stream.fromIterable([10000]), 279 | Stream.fromIterable([100000]), 280 | Stream.empty(), 281 | ); 282 | expect(stream, emits(emitsDone)); 283 | }); 284 | 285 | test('emits combined latest values from inputs', () { 286 | final controller1 = StreamController(); 287 | final controller2 = StreamController(); 288 | final controller3 = StreamController(); 289 | final controller4 = StreamController(); 290 | final controller5 = StreamController(); 291 | final stream = combineLatestTuple5( 292 | controller1.stream, 293 | controller2.stream, 294 | controller3.stream, 295 | controller4.stream, 296 | controller5.stream, 297 | ); 298 | controller1.add(1); 299 | controller1.add(2); 300 | controller2.add(100); 301 | controller2.add(1000); 302 | controller3.add(10000); 303 | controller4.add(100000); 304 | controller5.add(9000000); 305 | controller1.close(); 306 | controller2.close(); 307 | controller3.close(); 308 | controller4.close(); 309 | controller5.close(); 310 | expect( 311 | stream, 312 | emitsInOrder([ 313 | Tuple5(1, 100, 10000, 100000, 9000000), 314 | Tuple5(2, 100, 10000, 100000, 9000000), 315 | Tuple5(2, 1000, 10000, 100000, 9000000), 316 | emitsDone, 317 | ]), 318 | ); 319 | }); 320 | }); 321 | 322 | group('combineLatestTuple6', () { 323 | test('emits nothing when all input streams are empty', () { 324 | final stream = combineLatestTuple6( 325 | Stream.empty(), 326 | Stream.empty(), 327 | Stream.empty(), 328 | Stream.empty(), 329 | Stream.empty(), 330 | Stream.empty(), 331 | ); 332 | expect(stream, emits(emitsDone)); 333 | }); 334 | 335 | test('emits nothing when first input stream is empty', () { 336 | final stream = combineLatestTuple6( 337 | Stream.empty(), 338 | Stream.fromIterable([1, 2, 3]), 339 | Stream.fromIterable([100, 1000]), 340 | Stream.fromIterable([10000]), 341 | Stream.fromIterable([100000]), 342 | Stream.fromIterable([1000000]), 343 | ); 344 | expect(stream, emits(emitsDone)); 345 | }); 346 | 347 | test('emits nothing when second input stream is empty', () { 348 | final stream = combineLatestTuple6( 349 | Stream.fromIterable([1, 2, 3]), 350 | Stream.empty(), 351 | Stream.fromIterable([100, 1000]), 352 | Stream.fromIterable([10000]), 353 | Stream.fromIterable([100000]), 354 | Stream.fromIterable([1000000]), 355 | ); 356 | expect(stream, emits(emitsDone)); 357 | }); 358 | 359 | test('emits nothing when third input stream is empty', () { 360 | final stream = combineLatestTuple6( 361 | Stream.fromIterable([1, 2, 3]), 362 | Stream.fromIterable([100, 1000]), 363 | Stream.empty(), 364 | Stream.fromIterable([10000]), 365 | Stream.fromIterable([100000]), 366 | Stream.fromIterable([1000000]), 367 | ); 368 | expect(stream, emits(emitsDone)); 369 | }); 370 | 371 | test('emits nothing when fourth input stream is empty', () { 372 | final stream = combineLatestTuple6( 373 | Stream.fromIterable([1, 2, 3]), 374 | Stream.fromIterable([100, 1000]), 375 | Stream.fromIterable([10000]), 376 | Stream.empty(), 377 | Stream.fromIterable([100000]), 378 | Stream.fromIterable([1000000]), 379 | ); 380 | expect(stream, emits(emitsDone)); 381 | }); 382 | 383 | test('emits nothing when fifth input stream is empty', () { 384 | final stream = combineLatestTuple6( 385 | Stream.fromIterable([1, 2, 3]), 386 | Stream.fromIterable([100, 1000]), 387 | Stream.fromIterable([10000]), 388 | Stream.fromIterable([100000]), 389 | Stream.empty(), 390 | Stream.fromIterable([1000000]), 391 | ); 392 | expect(stream, emits(emitsDone)); 393 | }); 394 | 395 | test('emits nothing when sixth input stream is empty', () { 396 | final stream = combineLatestTuple6( 397 | Stream.fromIterable([1, 2, 3]), 398 | Stream.fromIterable([100, 1000]), 399 | Stream.fromIterable([10000]), 400 | Stream.fromIterable([100000]), 401 | Stream.fromIterable([1000000]), 402 | Stream.empty(), 403 | ); 404 | expect(stream, emits(emitsDone)); 405 | }); 406 | 407 | test('emits combined latest values from inputs', () { 408 | final controller1 = StreamController(); 409 | final controller2 = StreamController(); 410 | final controller3 = StreamController(); 411 | final controller4 = StreamController(); 412 | final controller5 = StreamController(); 413 | final controller6 = StreamController(); 414 | final stream = combineLatestTuple6( 415 | controller1.stream, 416 | controller2.stream, 417 | controller3.stream, 418 | controller4.stream, 419 | controller5.stream, 420 | controller6.stream, 421 | ); 422 | controller1.add(1); 423 | controller1.add(2); 424 | controller2.add(100); 425 | controller2.add(1000); 426 | controller3.add(10000); 427 | controller4.add(100000); 428 | controller5.add(9000000); 429 | controller6.add(123); 430 | controller1.close(); 431 | controller2.close(); 432 | controller3.close(); 433 | controller4.close(); 434 | controller5.close(); 435 | controller6.close(); 436 | expect( 437 | stream, 438 | emitsInOrder([ 439 | Tuple6(1, 100, 10000, 100000, 9000000, 123), 440 | Tuple6(2, 100, 10000, 100000, 9000000, 123), 441 | Tuple6(2, 1000, 10000, 100000, 9000000, 123), 442 | emitsDone, 443 | ]), 444 | ); 445 | }); 446 | }); 447 | 448 | group('combineLatestTuple7', () { 449 | test('emits nothing when all input streams are empty', () { 450 | final stream = combineLatestTuple7( 451 | Stream.empty(), 452 | Stream.empty(), 453 | Stream.empty(), 454 | Stream.empty(), 455 | Stream.empty(), 456 | Stream.empty(), 457 | Stream.empty(), 458 | ); 459 | expect(stream, emits(emitsDone)); 460 | }); 461 | 462 | test('emits nothing when first input stream is empty', () { 463 | final stream = combineLatestTuple7( 464 | Stream.empty(), 465 | Stream.fromIterable([1, 2, 3]), 466 | Stream.fromIterable([100, 1000]), 467 | Stream.fromIterable([10000]), 468 | Stream.fromIterable([100000]), 469 | Stream.fromIterable([1000000]), 470 | Stream.fromIterable([10000000]), 471 | ); 472 | expect(stream, emits(emitsDone)); 473 | }); 474 | 475 | test('emits nothing when second input stream is empty', () { 476 | final stream = combineLatestTuple7( 477 | Stream.fromIterable([1, 2, 3]), 478 | Stream.empty(), 479 | Stream.fromIterable([100, 1000]), 480 | Stream.fromIterable([10000]), 481 | Stream.fromIterable([100000]), 482 | Stream.fromIterable([1000000]), 483 | Stream.fromIterable([10000000]), 484 | ); 485 | expect(stream, emits(emitsDone)); 486 | }); 487 | 488 | test('emits nothing when third input stream is empty', () { 489 | final stream = combineLatestTuple7( 490 | Stream.fromIterable([1, 2, 3]), 491 | Stream.fromIterable([100, 1000]), 492 | Stream.empty(), 493 | Stream.fromIterable([10000]), 494 | Stream.fromIterable([100000]), 495 | Stream.fromIterable([1000000]), 496 | Stream.fromIterable([10000000]), 497 | ); 498 | expect(stream, emits(emitsDone)); 499 | }); 500 | 501 | test('emits nothing when fourth input stream is empty', () { 502 | final stream = combineLatestTuple7( 503 | Stream.fromIterable([1, 2, 3]), 504 | Stream.fromIterable([100, 1000]), 505 | Stream.fromIterable([10000]), 506 | Stream.empty(), 507 | Stream.fromIterable([100000]), 508 | Stream.fromIterable([1000000]), 509 | Stream.fromIterable([10000000]), 510 | ); 511 | expect(stream, emits(emitsDone)); 512 | }); 513 | 514 | test('emits nothing when fifth input stream is empty', () { 515 | final stream = combineLatestTuple7( 516 | Stream.fromIterable([1, 2, 3]), 517 | Stream.fromIterable([100, 1000]), 518 | Stream.fromIterable([10000]), 519 | Stream.fromIterable([100000]), 520 | Stream.empty(), 521 | Stream.fromIterable([1000000]), 522 | Stream.fromIterable([10000000]), 523 | ); 524 | expect(stream, emits(emitsDone)); 525 | }); 526 | 527 | test('emits nothing when sixth input stream is empty', () { 528 | final stream = combineLatestTuple7( 529 | Stream.fromIterable([1, 2, 3]), 530 | Stream.fromIterable([100, 1000]), 531 | Stream.fromIterable([10000]), 532 | Stream.fromIterable([100000]), 533 | Stream.fromIterable([1000000]), 534 | Stream.empty(), 535 | Stream.fromIterable([10000000]), 536 | ); 537 | expect(stream, emits(emitsDone)); 538 | }); 539 | 540 | test('emits nothing when seventh input stream is empty', () { 541 | final stream = combineLatestTuple7( 542 | Stream.fromIterable([1, 2, 3]), 543 | Stream.fromIterable([100, 1000]), 544 | Stream.fromIterable([10000]), 545 | Stream.fromIterable([100000]), 546 | Stream.fromIterable([1000000]), 547 | Stream.fromIterable([10000000]), 548 | Stream.empty(), 549 | ); 550 | expect(stream, emits(emitsDone)); 551 | }); 552 | 553 | test('emits combined latest values from inputs', () { 554 | final controller1 = StreamController(); 555 | final controller2 = StreamController(); 556 | final controller3 = StreamController(); 557 | final controller4 = StreamController(); 558 | final controller5 = StreamController(); 559 | final controller6 = StreamController(); 560 | final controller7 = StreamController(); 561 | final stream = combineLatestTuple7( 562 | controller1.stream, 563 | controller2.stream, 564 | controller3.stream, 565 | controller4.stream, 566 | controller5.stream, 567 | controller6.stream, 568 | controller7.stream, 569 | ); 570 | controller1.add(1); 571 | controller1.add(2); 572 | controller2.add(100); 573 | controller2.add(1000); 574 | controller3.add(10000); 575 | controller4.add(100000); 576 | controller5.add(9000000); 577 | controller6.add(123); 578 | controller7.add(456); 579 | controller1.close(); 580 | controller2.close(); 581 | controller3.close(); 582 | controller4.close(); 583 | controller5.close(); 584 | controller6.close(); 585 | controller7.close(); 586 | expect( 587 | stream, 588 | emitsInOrder([ 589 | Tuple7(1, 100, 10000, 100000, 9000000, 123, 456), 590 | Tuple7(2, 100, 10000, 100000, 9000000, 123, 456), 591 | Tuple7(2, 1000, 10000, 100000, 9000000, 123, 456), 592 | emitsDone, 593 | ]), 594 | ); 595 | }); 596 | }); 597 | } 598 | -------------------------------------------------------------------------------- /test/flow_converters_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:flutter_stream_extensions/flow_converters.dart'; 18 | import 'package:flutter_stream_extensions/stream_event.dart'; 19 | import 'package:test/test.dart'; 20 | 21 | void main() { 22 | group('convertStreamEvent', () { 23 | late StreamController controller; 24 | 25 | setUp(() { 26 | controller = StreamController(); 27 | }); 28 | 29 | tearDown(() { 30 | controller.close(); 31 | }); 32 | 33 | group('when converting to data', () { 34 | late Stream output; 35 | 36 | setUp(() { 37 | output = controller.stream.transform(convertStreamEvent(convertToData)); 38 | }); 39 | 40 | test('emits properly when input is also data', () { 41 | controller.add(123); 42 | controller.add(456); 43 | expect( 44 | output, 45 | emitsInOrder([ 46 | 'data 123', 47 | 'data 456', 48 | ]), 49 | ); 50 | }); 51 | 52 | test('emits properly when input is error', () { 53 | controller.addError('dog'); 54 | controller.addError('cat'); 55 | expect( 56 | output, 57 | emitsInOrder([ 58 | 'error dog', 59 | 'error cat', 60 | ]), 61 | ); 62 | }); 63 | }); 64 | 65 | group('when converting to error', () { 66 | late Stream output; 67 | 68 | setUp(() { 69 | output = 70 | controller.stream.transform(convertStreamEvent(convertToError)); 71 | }); 72 | 73 | test('emits properly when input is data', () { 74 | controller.add(123); 75 | controller.add(456); 76 | expect( 77 | output, 78 | emitsInOrder([ 79 | emitsError('123'), 80 | emitsError('456'), 81 | ]), 82 | ); 83 | }); 84 | 85 | test('emits properly when input is also error', () { 86 | controller.addError('dog'); 87 | controller.addError('cat'); 88 | expect( 89 | output, 90 | emitsInOrder([ 91 | emitsError('dog'), 92 | emitsError('cat'), 93 | ]), 94 | ); 95 | }); 96 | }); 97 | 98 | group('when converter throws', () { 99 | late Stream output; 100 | 101 | setUp(() { 102 | output = 103 | controller.stream.transform(convertStreamEvent(convertToError)); 104 | }); 105 | 106 | test('emits properly when input is data', () { 107 | controller.add(123); 108 | controller.add(456); 109 | expect( 110 | output, 111 | emitsInOrder([ 112 | emitsError('123'), 113 | emitsError('456'), 114 | ]), 115 | ); 116 | }); 117 | 118 | test('emits properly when input is error', () { 119 | controller.addError('dog'); 120 | controller.addError('cat'); 121 | expect( 122 | output, 123 | emitsInOrder([ 124 | emitsError('dog'), 125 | emitsError('cat'), 126 | ]), 127 | ); 128 | }); 129 | }); 130 | }); 131 | } 132 | 133 | StreamEvent convertToData(StreamEvent input) { 134 | if (input.hasData) { 135 | return StreamEvent.data('data ${input.data}'); 136 | } 137 | return StreamEvent.data('error ${input.error}'); 138 | } 139 | 140 | StreamEvent convertToError(StreamEvent input) { 141 | if (input.hasData) { 142 | return StreamEvent.error('${input.data}'); 143 | } else { 144 | return StreamEvent.error('${input.error}'); 145 | } 146 | } 147 | 148 | StreamEvent convertToThrows(StreamEvent input) { 149 | if (input.hasData) { 150 | throw '${input.data}'; 151 | } else { 152 | throw '${input.error}'; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /test/flow_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:auto_disposable/auto_disposable.dart'; 18 | import 'package:flutter_stream_extensions/flow.dart'; 19 | import 'package:flutter_stream_extensions/stream_event.dart'; 20 | import 'package:test/test.dart'; 21 | 22 | void main() { 23 | group('Node', () { 24 | group('disposal', () { 25 | test('does not forward events after disposal', () async { 26 | var counter = 0; 27 | final inputNode = Node() 28 | ..withInitialData(counter) 29 | ..wire0() 30 | ..asSettable(); 31 | final node = Node()..wire1(inputNode, (int i) => counter = i); 32 | await pumpEventQueue(); 33 | 34 | // access node.stream to ensure setup 35 | node.stream; 36 | 37 | inputNode.value = 1; 38 | await pumpEventQueue(); 39 | 40 | // node is wired and not disposed, counter should update. 41 | expect(counter, 1); 42 | 43 | node.dispose(); 44 | 45 | inputNode.value = 2; 46 | await pumpEventQueue(); 47 | 48 | // node is disposed, counter should *not* update. 49 | expect(counter, 1); 50 | 51 | inputNode.dispose(); 52 | }); 53 | }); 54 | 55 | group('before wiring', () { 56 | for (var testerFactory in [ 57 | SingleNodeTesterFactory( 58 | 'newly constructed', 59 | () => Node(), 60 | ), 61 | SingleNodeTesterFactory( 62 | 'with initial data', 63 | () => Node()..withInitialData(999), 64 | ), 65 | SingleNodeTesterFactory( 66 | 'with initial event', 67 | () => Node()..withInitialEvent(StreamEvent.error('cat')), 68 | ), 69 | SingleNodeTesterFactory( 70 | 'settable', 71 | () => Node()..asSettable(), 72 | ), 73 | SingleNodeTesterFactory( 74 | 'settable with initial data', 75 | () => Node() 76 | ..asSettable() 77 | ..withInitialData(999), 78 | ), 79 | SingleNodeTesterFactory( 80 | 'settable with initial event', 81 | () => Node() 82 | ..asSettable() 83 | ..withInitialEvent(StreamEvent.error('cat')), 84 | ), 85 | ]) { 86 | group(testerFactory.description, () { 87 | late NodeTester tester; 88 | 89 | setUp(() { 90 | tester = testerFactory.createTester(); 91 | }); 92 | 93 | tearDown(() => tester.dispose()); 94 | 95 | test('throws when getting stream', () { 96 | expect(() => tester.outputNode.stream, throwsStateError); 97 | }); 98 | 99 | test('throws when setting value', () { 100 | expect(() => tester.outputNode.value = 1, throwsStateError); 101 | }); 102 | 103 | test('throws when setting error', () { 104 | expect(() => tester.outputNode.error = 'dog', throwsStateError); 105 | }); 106 | 107 | test('throws when getting view', () { 108 | expect(() => tester.outputNode.view, throwsStateError); 109 | }); 110 | 111 | test('throws when subscribing', () { 112 | expect( 113 | () => tester.outputNode.subscribePermanently(print), 114 | throwsStateError, 115 | ); 116 | }); 117 | 118 | test('throws when subscribing data-only', () { 119 | expect( 120 | () => tester.outputNode.subscribeToDataPermanently(print), 121 | throwsStateError, 122 | ); 123 | }); 124 | }); 125 | } 126 | }); 127 | 128 | group('when not settable', () { 129 | for (var testerFactory in [ 130 | SingleNodeTesterFactory( 131 | 'wired with wire0', 132 | () => Node()..wire0(), 133 | ), 134 | SingleNodeTesterFactory( 135 | 'with initial data and wired with wire0', 136 | () => Node() 137 | ..withInitialData(999) 138 | ..wire0(), 139 | ), 140 | SingleNodeTesterFactory( 141 | 'with initial event and wired with wire0', 142 | () => Node() 143 | ..withInitialEvent(StreamEvent.error('cat')) 144 | ..wire0(), 145 | ), 146 | StreamInputNodeTesterFactory( 147 | 'wired as passthrough', 148 | (inputStream) => Node()..wireAsPassthrough(inputStream), 149 | ), 150 | StreamInputNodeTesterFactory( 151 | 'wired as transformed stream', 152 | (inputStream) => 153 | Node()..wireAsTransformedStream(inputStream, whereEven()), 154 | ), 155 | SingleNodeInputNodeTesterFactory( 156 | 'wired as passthrough node', 157 | (inputNode) => Node()..wireAsPassthroughNode(inputNode), 158 | ), 159 | SingleNodeInputNodeTesterFactory( 160 | 'wired as transformed node', 161 | (inputNode) => 162 | Node()..wireAsTransformedNode(inputNode, whereEven()), 163 | ), 164 | ]) { 165 | group(testerFactory.description, () { 166 | late NodeTester tester; 167 | 168 | setUp(() { 169 | tester = testerFactory.createTester(); 170 | }); 171 | 172 | tearDown(() => tester.dispose()); 173 | 174 | test('cannot be wired again', () { 175 | expectCannotBeWiredAgain(tester.outputNode); 176 | }); 177 | 178 | test('returns itself as the view', () { 179 | expect(tester.outputNode.view, same(tester.outputNode)); 180 | }); 181 | 182 | test('throws when setting value', () { 183 | expect(() => tester.outputNode.value = 1, throwsStateError); 184 | }); 185 | 186 | test('throws when setting error', () { 187 | expect(() => tester.outputNode.error = 'dog', throwsStateError); 188 | }); 189 | }); 190 | } 191 | }); 192 | 193 | group('when settable and wired', () { 194 | for (var testerFactory in [ 195 | SingleNodeTesterFactory( 196 | 'with wire0', 197 | () => Node() 198 | ..asSettable() 199 | ..wire0(), 200 | ), 201 | SingleNodeTesterFactory( 202 | 'with initial data and wire0', 203 | () => Node() 204 | ..asSettable() 205 | ..withInitialData(999) 206 | ..wire0(), 207 | ), 208 | SingleNodeTesterFactory( 209 | 'with initial event and wire0', 210 | () => Node() 211 | ..asSettable() 212 | ..withInitialEvent(StreamEvent.error('cat')) 213 | ..wire0(), 214 | ), 215 | StreamInputNodeTesterFactory( 216 | 'wired as passthrough', 217 | (inputStream) => Node() 218 | ..asSettable() 219 | ..wireAsPassthrough(inputStream), 220 | ), 221 | StreamInputNodeTesterFactory( 222 | 'wired as transformed stream', 223 | (inputStream) => Node() 224 | ..asSettable() 225 | ..wireAsTransformedStream(inputStream, whereEven()), 226 | ), 227 | SingleNodeInputNodeTesterFactory( 228 | 'wired as passthrough node', 229 | (inputNode) => Node() 230 | ..asSettable() 231 | ..wireAsPassthroughNode(inputNode), 232 | ), 233 | SingleNodeInputNodeTesterFactory( 234 | 'wired as transformed node', 235 | (inputNode) => Node() 236 | ..asSettable() 237 | ..wireAsTransformedNode(inputNode, whereEven()), 238 | ), 239 | ]) { 240 | group(testerFactory.description, () { 241 | late NodeTester tester; 242 | 243 | setUp(() { 244 | tester = testerFactory.createTester(); 245 | }); 246 | 247 | tearDown(() => tester.dispose()); 248 | 249 | test('cannot be wired again', () { 250 | expectCannotBeWiredAgain(tester.outputNode); 251 | }); 252 | 253 | test('returns an unsettable view', () { 254 | tester.outputNode.value = 101; 255 | final view = tester.outputNode.view; 256 | tester.outputNode.value = 102; 257 | expect(view, isNot(same(tester.outputNode))); 258 | expect(view.settable, isFalse); 259 | expect(view.stream, emits(102)); 260 | expect(view, same(tester.outputNode.view)); 261 | }); 262 | 263 | test('disposes of the view automatically', () { 264 | final view = tester.outputNode.view; 265 | tester.outputNode.dispose(); 266 | expect(view.isDisposed, isTrue); 267 | }); 268 | 269 | test('emits set events to subscriber', () async { 270 | final collector = EventCollector(tester.outputNode); 271 | 272 | tester.outputNode.value = 101; 273 | tester.outputNode.error = 'dog'; 274 | await pumpEventQueue(); 275 | 276 | expect( 277 | collector.emittedEvents.lastItems(2), 278 | [ 279 | StreamEvent.data(101), 280 | StreamEvent.error('dog'), 281 | ], 282 | ); 283 | expect(collector.emittedValues.last, 101); 284 | }); 285 | 286 | test('emits set events to stream', () async { 287 | expect( 288 | tester.outputNode.stream, 289 | emitsInOrder([ 290 | mayEmit(emitsError('cat')), 291 | mayEmit(999), 292 | 101, 293 | emitsError('dog'), 294 | ]), 295 | ); 296 | 297 | tester.outputNode.value = 101; 298 | tester.outputNode.error = 'dog'; 299 | await pumpEventQueue(); 300 | }); 301 | }); 302 | } 303 | }); 304 | 305 | group('with initial data wired', () { 306 | for (var testerFactory in [ 307 | SingleNodeTesterFactory( 308 | 'not settable with wire0', 309 | () => Node() 310 | ..withInitialData(999) 311 | ..wire0(), 312 | ), 313 | SingleNodeTesterFactory( 314 | 'settable with wire0', 315 | () => Node() 316 | ..asSettable() 317 | ..withInitialData(999) 318 | ..wire0(), 319 | ), 320 | StreamInputNodeTesterFactory( 321 | 'wired as passthrough', 322 | (inputStream) => Node() 323 | ..withInitialData(999) 324 | ..wireAsPassthrough(inputStream), 325 | ), 326 | StreamInputNodeTesterFactory( 327 | 'wired as transformed stream', 328 | (inputStream) => Node() 329 | ..withInitialData(999) 330 | ..wireAsTransformedStream(inputStream, whereEven()), 331 | ), 332 | SingleNodeInputNodeTesterFactory( 333 | 'wired as passthrough node', 334 | (inputNode) => Node() 335 | ..withInitialData(999) 336 | ..wireAsPassthroughNode(inputNode), 337 | ), 338 | SingleNodeInputNodeTesterFactory( 339 | 'wired as transformed node', 340 | (inputNode) => Node() 341 | ..withInitialData(999) 342 | ..wireAsTransformedNode(inputNode, whereEven()), 343 | ), 344 | ]) { 345 | group(testerFactory.description, () { 346 | late NodeTester tester; 347 | 348 | setUp(() { 349 | tester = testerFactory.createTester(); 350 | }); 351 | 352 | tearDown(() => tester.dispose()); 353 | 354 | test('emits initial event to subscriber', () async { 355 | final collector = EventCollector(tester.outputNode); 356 | await pumpEventQueue(); 357 | 358 | expect( 359 | collector.emittedEvents, 360 | [StreamEvent.data(999)], 361 | ); 362 | expect(collector.emittedValues, [999]); 363 | }); 364 | 365 | test('emits initial event to stream', () async { 366 | expect( 367 | tester.outputNode.stream, 368 | emits(999), 369 | ); 370 | 371 | await pumpEventQueue(); 372 | }); 373 | }); 374 | } 375 | }); 376 | 377 | group('with initial error and wired', () { 378 | for (var testerFactory in [ 379 | SingleNodeTesterFactory( 380 | 'not settable with wire0', 381 | () => Node() 382 | ..withInitialEvent(StreamEvent.error('cat')) 383 | ..wire0(), 384 | ), 385 | SingleNodeTesterFactory( 386 | 'settable with wire0', 387 | () => Node() 388 | ..asSettable() 389 | ..withInitialEvent(StreamEvent.error('cat')) 390 | ..wire0(), 391 | ), 392 | StreamInputNodeTesterFactory( 393 | 'wired as passthrough', 394 | (inputStream) => Node() 395 | ..withInitialEvent(StreamEvent.error('cat')) 396 | ..wireAsPassthrough(inputStream), 397 | ), 398 | StreamInputNodeTesterFactory( 399 | 'wired as transformed stream', 400 | (inputStream) => Node() 401 | ..withInitialEvent(StreamEvent.error('cat')) 402 | ..wireAsTransformedStream(inputStream, whereEven()), 403 | ), 404 | SingleNodeInputNodeTesterFactory( 405 | 'wired as passthrough node', 406 | (inputNode) => Node() 407 | ..withInitialEvent(StreamEvent.error('cat')) 408 | ..wireAsPassthroughNode(inputNode), 409 | ), 410 | SingleNodeInputNodeTesterFactory( 411 | 'wired as transformed node', 412 | (inputNode) => Node() 413 | ..withInitialEvent(StreamEvent.error('cat')) 414 | ..wireAsTransformedNode(inputNode, whereEven()), 415 | ), 416 | ]) { 417 | group(testerFactory.description, () { 418 | late NodeTester tester; 419 | 420 | setUp(() { 421 | tester = testerFactory.createTester(); 422 | }); 423 | 424 | tearDown(() => tester.dispose()); 425 | 426 | test('emits initial error to subscriber', () async { 427 | final collector = EventCollector(tester.outputNode); 428 | 429 | await pumpEventQueue(); 430 | 431 | expect( 432 | collector.emittedEvents, 433 | [StreamEvent.error('cat')], 434 | ); 435 | expect(collector.emittedValues, []); 436 | }); 437 | 438 | test('emits initial event to stream', () async { 439 | expect( 440 | tester.outputNode.stream, 441 | emitsError('cat'), 442 | ); 443 | 444 | await pumpEventQueue(); 445 | }); 446 | }); 447 | } 448 | }); 449 | 450 | group('with no initial value or error and wired with [wire0]', () { 451 | for (var testerFactory in [ 452 | SingleNodeTesterFactory( 453 | 'not settable', 454 | () => Node()..wire0(), 455 | ), 456 | SingleNodeTesterFactory( 457 | 'settable', 458 | () => Node() 459 | ..asSettable() 460 | ..wire0(), 461 | ), 462 | ]) { 463 | group(testerFactory.description, () { 464 | late NodeTester tester; 465 | 466 | setUp(() { 467 | tester = testerFactory.createTester(); 468 | }); 469 | 470 | tearDown(() => tester.dispose()); 471 | 472 | test('emits nothing subscriber', () async { 473 | final collector = EventCollector(tester.outputNode); 474 | 475 | await pumpEventQueue(); 476 | 477 | expect( 478 | collector.emittedEvents, 479 | [], 480 | ); 481 | expect(collector.emittedValues, []); 482 | }); 483 | 484 | test('emits nothing to stream', () async { 485 | await expectStreamEmitsNothing(tester.outputNode.stream); 486 | }); 487 | }); 488 | } 489 | }); 490 | 491 | group('wired as passthrough', () { 492 | for (var testerFactory in [ 493 | StreamInputNodeTesterFactory( 494 | 'no initial data', 495 | (inputStream) => Node()..wireAsPassthrough(inputStream), 496 | ), 497 | StreamInputNodeTesterFactory( 498 | 'with initial data', 499 | (inputStream) => Node() 500 | ..withInitialData(10) 501 | ..wireAsPassthrough(inputStream), 502 | ), 503 | StreamInputNodeTesterFactory( 504 | 'with initial error', 505 | (inputStream) => Node() 506 | ..withInitialEvent(StreamEvent.error('initial error')) 507 | ..wireAsPassthrough(inputStream), 508 | ), 509 | ]) { 510 | group(testerFactory.description, () { 511 | late StreamInputNodeTester tester; 512 | 513 | setUp(() { 514 | tester = testerFactory.createTester(); 515 | }); 516 | 517 | tearDown(() => tester.dispose()); 518 | 519 | // A sequence of stream updates shared between the tests below. 520 | void triggerStreamUpdates() { 521 | tester.inputStreamController.add(20); 522 | tester.inputStreamController.add(25); 523 | tester.inputStreamController.addError('Out of range', StackTrace.empty); 524 | } 525 | 526 | test('emits input stream events to subscriber', () async { 527 | final collector = EventCollector(tester.outputNode); 528 | 529 | triggerStreamUpdates(); 530 | await pumpEventQueue(); 531 | 532 | expect( 533 | collector.emittedEvents.lastItems(3), 534 | [ 535 | StreamEvent.data(20), 536 | StreamEvent.data(25), 537 | StreamEvent.error('Out of range'), 538 | ], 539 | ); 540 | expect(collector.emittedValues.lastItems(2), [20, 25]); 541 | }); 542 | 543 | test('emits input stream events to stream', () async { 544 | expect( 545 | tester.outputNode.stream, 546 | emitsInOrder([ 547 | mayEmit(emitsError('initial error')), 548 | mayEmit(10), 549 | 20, 550 | 25, 551 | emitsError('Out of range'), 552 | ]), 553 | ); 554 | 555 | triggerStreamUpdates(); 556 | await pumpEventQueue(); 557 | }); 558 | }); 559 | } 560 | }); 561 | 562 | group('wired as transformed stream', () { 563 | for (var testerFactory in [ 564 | StreamInputNodeTesterFactory( 565 | 'no initial data', 566 | (inputStream) => 567 | Node()..wireAsTransformedStream(inputStream, whereEven()), 568 | ), 569 | StreamInputNodeTesterFactory( 570 | 'with initial data', 571 | (inputStream) => Node() 572 | ..withInitialData(10) 573 | ..wireAsTransformedStream(inputStream, whereEven()), 574 | ), 575 | StreamInputNodeTesterFactory( 576 | 'with initial error', 577 | (inputStream) => Node() 578 | ..withInitialEvent(StreamEvent.error('initial error')) 579 | ..wireAsTransformedStream(inputStream, whereEven()), 580 | ), 581 | ]) { 582 | group(testerFactory.description, () { 583 | late StreamInputNodeTester tester; 584 | 585 | setUp(() { 586 | tester = testerFactory.createTester(); 587 | }); 588 | 589 | tearDown(() => tester.dispose()); 590 | 591 | // A sequence of stream updates shared between the tests below. 592 | void triggerStreamUpdates() { 593 | tester.inputStreamController.add(20); 594 | tester.inputStreamController.add(21); 595 | tester.inputStreamController.add(22); 596 | tester.inputStreamController.add(23); 597 | tester.inputStreamController.addError('Out of range', StackTrace.empty); 598 | } 599 | 600 | test('emits transformed input stream events to subscriber', () async { 601 | final collector = EventCollector(tester.outputNode); 602 | 603 | triggerStreamUpdates(); 604 | await pumpEventQueue(); 605 | 606 | expect( 607 | collector.emittedEvents.lastItems(3), 608 | [ 609 | StreamEvent.data(20), 610 | StreamEvent.data(22), 611 | StreamEvent.error('Out of range'), 612 | ], 613 | ); 614 | expect(collector.emittedValues.lastItems(2), [20, 22]); 615 | }); 616 | 617 | test('emits transformed input stream events to stream', () async { 618 | expect( 619 | tester.outputNode.stream, 620 | emitsInOrder([ 621 | mayEmit(emitsError('initial error')), 622 | mayEmit(10), 623 | 20, 624 | 22, 625 | emitsError('Out of range'), 626 | ]), 627 | ); 628 | 629 | triggerStreamUpdates(); 630 | await pumpEventQueue(); 631 | }); 632 | }); 633 | } 634 | }); 635 | 636 | group('wired as passthrough node', () { 637 | for (var testerFactory in [ 638 | SingleNodeInputNodeTesterFactory( 639 | 'no initial data', 640 | (inputNode) => Node()..wireAsPassthroughNode(inputNode), 641 | ), 642 | SingleNodeInputNodeTesterFactory( 643 | 'with initial data', 644 | (inputNode) => Node() 645 | ..withInitialData(10) 646 | ..wireAsPassthroughNode(inputNode), 647 | ), 648 | SingleNodeInputNodeTesterFactory( 649 | 'with initial error', 650 | (inputNode) => Node() 651 | ..withInitialEvent(StreamEvent.error('initial error')) 652 | ..wireAsPassthroughNode(inputNode), 653 | ), 654 | ]) { 655 | group(testerFactory.description, () { 656 | late SingleNodeInputNodeTester tester; 657 | 658 | setUp(() { 659 | tester = testerFactory.createTester(); 660 | }); 661 | 662 | tearDown(() => tester.dispose()); 663 | 664 | // A sequence of stream updates shared between the tests below. 665 | void triggerStreamUpdates() { 666 | tester.inputNode.value = 20; 667 | tester.inputNode.value = 25; 668 | tester.inputNode.error = 'Out of range'; 669 | } 670 | 671 | test('emits input stream events to subscriber', () async { 672 | final collector = EventCollector(tester.outputNode); 673 | 674 | triggerStreamUpdates(); 675 | await pumpEventQueue(); 676 | 677 | expect( 678 | collector.emittedEvents.lastItems(3), 679 | [ 680 | StreamEvent.data(20), 681 | StreamEvent.data(25), 682 | StreamEvent.error('Out of range'), 683 | ], 684 | ); 685 | expect(collector.emittedValues.lastItems(2), [20, 25]); 686 | }); 687 | 688 | test('emits input stream events to stream', () async { 689 | expect( 690 | tester.outputNode.stream, 691 | emitsInOrder([ 692 | mayEmit(emitsError('initial error')), 693 | mayEmit(10), 694 | 20, 695 | 25, 696 | emitsError('Out of range'), 697 | ]), 698 | ); 699 | 700 | triggerStreamUpdates(); 701 | await pumpEventQueue(); 702 | }); 703 | }); 704 | } 705 | }); 706 | 707 | group('wired as transformed node', () { 708 | for (var testerFactory in [ 709 | SingleNodeInputNodeTesterFactory( 710 | 'no initial data', 711 | (inputNode) => 712 | Node()..wireAsTransformedNode(inputNode, whereEven()), 713 | ), 714 | SingleNodeInputNodeTesterFactory( 715 | 'with initial data', 716 | (inputNode) => Node() 717 | ..withInitialData(10) 718 | ..wireAsTransformedNode(inputNode, whereEven()), 719 | ), 720 | SingleNodeInputNodeTesterFactory( 721 | 'with initial error', 722 | (inputNode) => Node() 723 | ..withInitialEvent(StreamEvent.error('initial error')) 724 | ..wireAsTransformedNode(inputNode, whereEven()), 725 | ), 726 | ]) { 727 | group(testerFactory.description, () { 728 | late SingleNodeInputNodeTester tester; 729 | 730 | setUp(() { 731 | tester = testerFactory.createTester(); 732 | }); 733 | 734 | tearDown(() => tester.dispose()); 735 | 736 | // A sequence of stream updates shared between the tests below. 737 | void triggerStreamUpdates() { 738 | tester.inputNode.value = 20; 739 | tester.inputNode.value = 21; 740 | tester.inputNode.value = 22; 741 | tester.inputNode.value = 23; 742 | 743 | tester.inputNode.error = 'Out of range'; 744 | } 745 | 746 | test('emits transformed input stream events to subscriber', () async { 747 | final collector = EventCollector(tester.outputNode); 748 | 749 | triggerStreamUpdates(); 750 | await pumpEventQueue(); 751 | 752 | expect( 753 | collector.emittedEvents.lastItems(3), 754 | [ 755 | StreamEvent.data(20), 756 | StreamEvent.data(22), 757 | StreamEvent.error('Out of range'), 758 | ], 759 | ); 760 | expect(collector.emittedValues.lastItems(2), [20, 22]); 761 | }); 762 | 763 | test('emits transformed input stream events to stream', () async { 764 | expect( 765 | tester.outputNode.stream, 766 | emitsInOrder([ 767 | mayEmit(emitsError('initial error')), 768 | mayEmit(10), 769 | 20, 770 | 22, 771 | emitsError('Out of range'), 772 | ]), 773 | ); 774 | 775 | triggerStreamUpdates(); 776 | await pumpEventQueue(); 777 | }); 778 | }); 779 | } 780 | }); 781 | 782 | // Test that data can flow from N input nodes to a single output node for 783 | // every wireN configuration. 784 | for (var testerFactory in [ 785 | // Tests all wireN methods in a basic way 786 | WiredNodeTesterFactory( 787 | description: 'when wired with wire1', 788 | numInputs: 1, 789 | ), 790 | WiredNodeTesterFactory( 791 | description: 'when wired with wire2', 792 | numInputs: 2, 793 | ), 794 | WiredNodeTesterFactory( 795 | description: 'when wired with wire3', 796 | numInputs: 3, 797 | ), 798 | WiredNodeTesterFactory( 799 | description: 'when wired with wire4', 800 | numInputs: 4, 801 | ), 802 | WiredNodeTesterFactory( 803 | description: 'when wired with wire5', 804 | numInputs: 5, 805 | ), 806 | WiredNodeTesterFactory( 807 | description: 'when wired with wire6', 808 | numInputs: 6, 809 | ), 810 | WiredNodeTesterFactory( 811 | description: 'when wired with wire7', 812 | numInputs: 7, 813 | ), 814 | 815 | // Test [wire7] with several different configurations as a proxy for all 816 | // the other wireN methods. 817 | WiredNodeTesterFactory( 818 | description: 'when wired with wire7 and with initial data', 819 | numInputs: 3, 820 | initialData: 'initial data', 821 | ), 822 | WiredNodeTesterFactory( 823 | description: 'when wired with wire7 and with initial error', 824 | numInputs: 3, 825 | initialEvent: StreamEvent.error('initial error'), 826 | ), 827 | ]) { 828 | group(testerFactory.description, () { 829 | late WiredNodeTester tester; 830 | setUp(() { 831 | tester = testerFactory.createTester(); 832 | }); 833 | 834 | tearDown(() => tester.dispose()); 835 | 836 | test('cannot be wired again', () { 837 | var inputNode = Node() 838 | ..asSettable() 839 | ..wire0(); 840 | expect( 841 | () => tester.outputNode.wire1(inputNode, (a) => a), 842 | throwsStateError, 843 | ); 844 | }); 845 | 846 | test('throws when setting value', () { 847 | expect(() => tester.outputNode.value = 'value', throwsStateError); 848 | }); 849 | 850 | test('throws when setting error', () { 851 | expect( 852 | () => tester.outputNode.error = 'error description', 853 | throwsStateError, 854 | ); 855 | }); 856 | 857 | // A sequence of node updates shared between the tests below. 858 | void triggerNodeUpdates() { 859 | // Set each node to 'a', 'b', 'c'... etc. 860 | tester.setAscendingAlphabetValuesOnInputs(); 861 | 862 | // Update first node to 'A'. 863 | tester.inputNodes[0].value = 'A'; 864 | 865 | // Update last node to 'Z'. 866 | tester.inputNodes[testerFactory.numInputs - 1].value = 'Z'; 867 | 868 | // Set error on first node. 869 | tester.inputNodes[0].error = 'broken'; 870 | } 871 | 872 | // Expected output events for the input events above. 873 | 874 | final alphabet = 'abcdefg'; 875 | 876 | // The first event should contain 'a', 'b', 'c' etc for N input nodes. 877 | final expectedOutput1 = alphabet.substring(0, testerFactory.numInputs); 878 | 879 | // Replace the first 'a' with 'A'. 880 | final expectedOutput2 = 'A' + expectedOutput1.substring(1); 881 | 882 | // Replace the last letter with 'Z'. 883 | final expectedOutput3 = 884 | expectedOutput2.substring(0, expectedOutput2.length - 1) + 'Z'; 885 | 886 | // Emit an error that overrides. 887 | final expectedError4 = 'broken'; 888 | 889 | test('forwards converted input', () async { 890 | expect( 891 | tester.outputNode.stream, 892 | emitsInOrder([ 893 | if (testerFactory.initialData != null) testerFactory.initialData!, 894 | if (testerFactory.initialEvent != null) 895 | emitsError('initial error'), 896 | expectedOutput1, 897 | expectedOutput2, 898 | expectedOutput3, 899 | emitsError(expectedError4), 900 | ]), 901 | ); 902 | 903 | triggerNodeUpdates(); 904 | await pumpEventQueue(); 905 | }); 906 | 907 | test('forwards converted input to subscribers', () async { 908 | final collector = EventCollector(tester.outputNode); 909 | 910 | triggerNodeUpdates(); 911 | await pumpEventQueue(); 912 | 913 | expect( 914 | collector.emittedEvents, 915 | [ 916 | if (testerFactory.initialData != null) 917 | StreamEvent.data(testerFactory.initialData!), 918 | if (testerFactory.initialEvent != null) 919 | testerFactory.initialEvent, 920 | StreamEvent.data(expectedOutput1), 921 | StreamEvent.data(expectedOutput2), 922 | StreamEvent.data(expectedOutput3), 923 | StreamEvent.error(expectedError4), 924 | ], 925 | ); 926 | 927 | expect(collector.emittedValues, [ 928 | if (testerFactory.initialData != null) testerFactory.initialData, 929 | expectedOutput1, 930 | expectedOutput2, 931 | expectedOutput3, 932 | ]); 933 | }); 934 | }); 935 | } 936 | 937 | // Test that the ability to set values on nodes directly still works for 938 | // nodes that are both settable *and* have wired inputs. wire3 is 939 | // arbitrarily chosen as a representative test case for all wireN methods. 940 | group('when wired to input nodes and also settable', () { 941 | late WiredNodeTester tester; 942 | setUp(() { 943 | tester = WiredNodeTester(numInputs: 3, settable: true); 944 | }); 945 | 946 | tearDown(() => tester.dispose()); 947 | 948 | // A sequence of node updates shared between the tests below. 949 | void triggerNodeUpdates() { 950 | tester.outputNode.value = 'output node value'; 951 | tester.outputNode.error = 'output node error'; 952 | tester.inputNodes[0].value = 'valueA'; 953 | tester.inputNodes[1].value = 'valueB'; 954 | tester.inputNodes[2].value = 'valueC'; 955 | tester.inputNodes[0].error = 'input node error'; 956 | tester.outputNode.value = 'output node later value'; 957 | tester.outputNode.error = 'output node later error'; 958 | } 959 | 960 | test('forwards converted input', () async { 961 | expect( 962 | tester.outputNode.stream, 963 | emitsInOrder([ 964 | 'output node value', 965 | emitsError('output node error'), 966 | 'valueAvalueBvalueC', 967 | emitsError('input node error'), 968 | 'output node later value', 969 | emitsError('output node later error'), 970 | ]), 971 | ); 972 | 973 | triggerNodeUpdates(); 974 | 975 | await pumpEventQueue(); 976 | }); 977 | 978 | test('forwards converted input to subscribers', () async { 979 | final collector = EventCollector(tester.outputNode); 980 | 981 | triggerNodeUpdates(); 982 | 983 | expect( 984 | collector.emittedEvents, 985 | [ 986 | StreamEvent.data('output node value'), 987 | StreamEvent.error('output node error'), 988 | StreamEvent.data('valueAvalueBvalueC'), 989 | StreamEvent.error('input node error'), 990 | StreamEvent.data('output node later value'), 991 | StreamEvent.error('output node later error'), 992 | ], 993 | ); 994 | 995 | expect(collector.emittedValues, [ 996 | 'output node value', 997 | 'valueAvalueBvalueC', 998 | 'output node later value', 999 | ]); 1000 | }); 1001 | }); 1002 | 1003 | // Test that values are still propagated correctly for nodes wired with an 1004 | // async converter function. Wire2 is arbitrarily chosen as a representative 1005 | // test case for all wireN methods. 1006 | group('when wired to inputs nodes with an async converter function', () { 1007 | late Node inputNode1; 1008 | late Node inputNode2; 1009 | late Node outputNode; 1010 | 1011 | setUp(() { 1012 | inputNode1 = Node() 1013 | ..wire0() 1014 | ..asSettable(); 1015 | inputNode2 = Node() 1016 | ..wire0() 1017 | ..asSettable(); 1018 | outputNode = Node() 1019 | ..wire2(inputNode1, inputNode2, (v0, v1) async { 1020 | return v0 + v1; 1021 | }); 1022 | }); 1023 | 1024 | tearDown(() { 1025 | inputNode1.dispose(); 1026 | inputNode2.dispose(); 1027 | outputNode.dispose(); 1028 | }); 1029 | 1030 | // A sequence of node updates shared between the tests below. 1031 | Future triggerNodeUpdates() async { 1032 | // Note that due to the asynchronous converter function, you need to 1033 | // pump the event queue after every event to ensure results are received 1034 | // in order. 1035 | 1036 | inputNode1.value = 'a'; 1037 | inputNode2.value = '1'; 1038 | await pumpEventQueue(); 1039 | 1040 | inputNode1.value = 'b'; 1041 | await pumpEventQueue(); 1042 | 1043 | inputNode2.value = '2'; 1044 | await pumpEventQueue(); 1045 | 1046 | inputNode1.error = 'error'; 1047 | await pumpEventQueue(); 1048 | 1049 | inputNode1.value = 'c'; 1050 | await pumpEventQueue(); 1051 | } 1052 | 1053 | test('forwards converted input', () async { 1054 | expect( 1055 | outputNode.stream, 1056 | emitsInOrder([ 1057 | 'a1', 1058 | 'b1', 1059 | 'b2', 1060 | emitsError('error'), 1061 | 'c2', 1062 | ]), 1063 | ); 1064 | 1065 | await triggerNodeUpdates(); 1066 | }); 1067 | 1068 | test('forwards converted input to subscribers', () async { 1069 | final collector = EventCollector(outputNode); 1070 | 1071 | await triggerNodeUpdates(); 1072 | 1073 | expect( 1074 | collector.emittedEvents, 1075 | [ 1076 | StreamEvent.data('a1'), 1077 | StreamEvent.data('b1'), 1078 | StreamEvent.data('b2'), 1079 | StreamEvent.error('error'), 1080 | StreamEvent.data('c2'), 1081 | ], 1082 | ); 1083 | 1084 | expect(collector.emittedValues, [ 1085 | 'a1', 1086 | 'b1', 1087 | 'b2', 1088 | 'c2', 1089 | ]); 1090 | }); 1091 | }); 1092 | }); 1093 | } 1094 | 1095 | /// A function that returns a Node. 1096 | typedef NodeProvider = Node Function(); 1097 | 1098 | /// A node tester which provides a main 'outputNode' under test. 1099 | abstract class NodeTester implements Disposable { 1100 | Node get outputNode; 1101 | } 1102 | 1103 | /// A factory to create a [NodeTester]. 1104 | /// 1105 | /// Tests should be parameterized with [NodeTesterFactory] instances rather than 1106 | /// [NodeTester] so that the latter can easily be regenerated in a [setUp] 1107 | /// function before each test method and to provide a description of the type of 1108 | /// tester being created. 1109 | abstract class NodeTesterFactory { 1110 | /// A description of the tester being created. 1111 | String get description; 1112 | 1113 | /// Creates and returns a new [WiredNodeTester] to be used in a single test. 1114 | NodeTester createTester(); 1115 | } 1116 | 1117 | /// A factory that creates a particular type of [Node] for testing. 1118 | class SingleNodeTesterFactory implements NodeTesterFactory { 1119 | SingleNodeTesterFactory( 1120 | this.description, 1121 | this.nodeProvider, 1122 | ); 1123 | 1124 | @override 1125 | final String description; 1126 | 1127 | /// Function which creates a node to be tested. 1128 | final NodeProvider nodeProvider; 1129 | 1130 | @override 1131 | SingleNodeTester createTester() => SingleNodeTester(nodeProvider()); 1132 | } 1133 | 1134 | class SingleNodeTester implements NodeTester { 1135 | @override 1136 | late final Node outputNode; 1137 | 1138 | SingleNodeTester(this.outputNode); 1139 | 1140 | @override 1141 | void dispose() { 1142 | outputNode.dispose(); 1143 | } 1144 | } 1145 | 1146 | typedef StreamInputNodeProvider = Node Function(Stream inputStream); 1147 | 1148 | /// A factory for a node tester with a node that is wired from an input stream. 1149 | /// 1150 | /// This deserves its own kind of factory because the lifecycle of the input 1151 | /// stream has to be managed which is non-trivial because the future returned by 1152 | /// [StreamController.close()] never returns if it hasn't been wired. 1153 | /// See https://github.com/dart-lang/sdk/issues/19095. 1154 | class StreamInputNodeTesterFactory implements NodeTesterFactory { 1155 | StreamInputNodeTesterFactory( 1156 | this.description, 1157 | this.nodeProvider, 1158 | ); 1159 | 1160 | @override 1161 | final String description; 1162 | 1163 | /// Function which creates the node to be tested, wiring it to the input 1164 | /// stream. 1165 | final StreamInputNodeProvider nodeProvider; 1166 | 1167 | @override 1168 | StreamInputNodeTester createTester() { 1169 | final inputStreamController = StreamController(); 1170 | return StreamInputNodeTester( 1171 | nodeProvider(inputStreamController.stream), 1172 | inputStreamController, 1173 | ); 1174 | } 1175 | } 1176 | 1177 | /// A node tester for a node with an input stream i.e. passthrough or 1178 | /// transformed. 1179 | class StreamInputNodeTester implements NodeTester { 1180 | @override 1181 | late final Node outputNode; 1182 | 1183 | StreamController inputStreamController; 1184 | 1185 | StreamInputNodeTester(this.outputNode, this.inputStreamController); 1186 | 1187 | @override 1188 | void dispose() { 1189 | outputNode.dispose(); 1190 | inputStreamController.close(); 1191 | } 1192 | } 1193 | 1194 | typedef SingleNodeInputNodeProvider = Node Function(Node inputNode); 1195 | 1196 | /// A factory for a node tester with a node that is wired from a single settable 1197 | /// input node. 1198 | class SingleNodeInputNodeTesterFactory implements NodeTesterFactory { 1199 | SingleNodeInputNodeTesterFactory( 1200 | this.description, 1201 | this.nodeProvider, 1202 | ); 1203 | 1204 | @override 1205 | final String description; 1206 | 1207 | /// Function which creates the node to be tested, wiring it to the input node. 1208 | final SingleNodeInputNodeProvider nodeProvider; 1209 | 1210 | @override 1211 | SingleNodeInputNodeTester createTester() { 1212 | final inputNode = Node() 1213 | ..asSettable() 1214 | ..wire0(); 1215 | return SingleNodeInputNodeTester( 1216 | nodeProvider(inputNode), 1217 | inputNode, 1218 | ); 1219 | } 1220 | } 1221 | 1222 | /// A node tester for a node with a single settable input node. 1223 | class SingleNodeInputNodeTester implements NodeTester { 1224 | @override 1225 | late final Node outputNode; 1226 | 1227 | Node inputNode; 1228 | 1229 | SingleNodeInputNodeTester(this.outputNode, this.inputNode); 1230 | 1231 | @override 1232 | void dispose() { 1233 | outputNode.dispose(); 1234 | inputNode.dispose(); 1235 | } 1236 | } 1237 | 1238 | /// A factory to create a [WiredNodeTester]. 1239 | class WiredNodeTesterFactory implements NodeTesterFactory { 1240 | @override 1241 | String description; 1242 | 1243 | /// The number of input nodes to create and wire to the output node. 1244 | int numInputs; 1245 | 1246 | /// Initial data to populate on the output node. 1247 | String? initialData; 1248 | 1249 | /// An initial event to populate on the output node. 1250 | StreamEvent? initialEvent; 1251 | 1252 | /// Whether the output node should be settable. 1253 | bool settable = false; 1254 | 1255 | WiredNodeTesterFactory({ 1256 | required this.description, 1257 | required this.numInputs, 1258 | this.initialData, 1259 | this.initialEvent, 1260 | this.settable = false, 1261 | }); 1262 | 1263 | @override 1264 | WiredNodeTester createTester() => WiredNodeTester( 1265 | numInputs: numInputs, 1266 | initialData: initialData, 1267 | initialEvent: initialEvent, 1268 | settable: settable, 1269 | ); 1270 | } 1271 | 1272 | /// A tester object that creates and wires N [inputNodes] to a [outputNode] 1273 | /// under test. 1274 | class WiredNodeTester implements Disposable, NodeTester { 1275 | final List> inputNodes = []; 1276 | 1277 | @override 1278 | late final Node outputNode = Node(); 1279 | 1280 | /// Creates a new tester with the given parameters. See 1281 | /// [WiredNodeTesterFactory] for parameter documentation. 1282 | WiredNodeTester({ 1283 | required int numInputs, 1284 | String? initialData, 1285 | StreamEvent? initialEvent, 1286 | bool settable = false, 1287 | }) { 1288 | for (var i = 0; i < numInputs; i++) { 1289 | var node = Node() 1290 | ..asSettable() 1291 | ..wire0(); 1292 | inputNodes.add(node); 1293 | } 1294 | 1295 | if (initialData != null) { 1296 | outputNode.withInitialData(initialData); 1297 | } else if (initialEvent != null) { 1298 | outputNode.withInitialEvent(initialEvent); 1299 | } 1300 | 1301 | if (settable) { 1302 | outputNode.asSettable(); 1303 | } 1304 | 1305 | switch (numInputs) { 1306 | case 1: 1307 | outputNode.wire1(inputNodes[0], (v1) => v1); 1308 | break; 1309 | case 2: 1310 | outputNode.wire2( 1311 | inputNodes[0], 1312 | inputNodes[1], 1313 | (v1, v2) => v1 + v2, 1314 | ); 1315 | break; 1316 | case 3: 1317 | outputNode.wire3( 1318 | inputNodes[0], 1319 | inputNodes[1], 1320 | inputNodes[2], 1321 | (v1, v2, v3) => v1 + v2 + v3, 1322 | ); 1323 | break; 1324 | case 4: 1325 | outputNode.wire4( 1326 | inputNodes[0], 1327 | inputNodes[1], 1328 | inputNodes[2], 1329 | inputNodes[3], 1330 | (v1, v2, v3, v4) => v1 + v2 + v3 + v4, 1331 | ); 1332 | break; 1333 | case 5: 1334 | outputNode.wire5( 1335 | inputNodes[0], 1336 | inputNodes[1], 1337 | inputNodes[2], 1338 | inputNodes[3], 1339 | inputNodes[4], 1340 | (v1, v2, v3, v4, v5) => v1 + v2 + v3 + v4 + v5, 1341 | ); 1342 | break; 1343 | case 6: 1344 | outputNode.wire6( 1345 | inputNodes[0], 1346 | inputNodes[1], 1347 | inputNodes[2], 1348 | inputNodes[3], 1349 | inputNodes[4], 1350 | inputNodes[5], 1351 | (v1, v2, v3, v4, v5, v6) => v1 + v2 + v3 + v4 + v5 + v6, 1352 | ); 1353 | break; 1354 | case 7: 1355 | outputNode 1356 | .wire7( 1357 | inputNodes[0], 1358 | inputNodes[1], 1359 | inputNodes[2], 1360 | inputNodes[3], 1361 | inputNodes[4], 1362 | inputNodes[5], 1363 | inputNodes[6], 1364 | (v1, v2, v3, v4, v5, v6, v7) => v1 + v2 + v3 + v4 + v5 + v6 + v7, 1365 | ); 1366 | break; 1367 | 1368 | default: 1369 | throw FallThroughError(); 1370 | } 1371 | } 1372 | 1373 | @override 1374 | void dispose() { 1375 | for (var inputNode in inputNodes) { 1376 | inputNode.dispose(); 1377 | } 1378 | outputNode.dispose(); 1379 | } 1380 | 1381 | /// Sets the strings 'a', 'b', 'c'... etc on the input nodes successively. 1382 | void setAscendingAlphabetValuesOnInputs() { 1383 | var i = 0; 1384 | for (var node in inputNodes) { 1385 | node.value = String.fromCharCode('a'.codeUnitAt(0) + i); 1386 | i++; 1387 | } 1388 | } 1389 | } 1390 | 1391 | /// Collects and stores events as both a subscriber and data subscriber to a 1392 | /// [Node]. 1393 | class EventCollector { 1394 | final Node node; 1395 | 1396 | /// Values emitted via [subscribePermanently] in chronological order. 1397 | final emittedValues = []; 1398 | 1399 | /// Values emitted via [subscribeToDataPermanently] in chronological order. 1400 | final emittedEvents = >[]; 1401 | 1402 | EventCollector(this.node) { 1403 | node.subscribePermanently(emittedEvents.add); 1404 | node.subscribeToDataPermanently(emittedValues.add); 1405 | } 1406 | } 1407 | 1408 | Future expectStreamEmitsNothing(Stream stream) async { 1409 | final emitted = >[]; 1410 | final listener = stream.listen( 1411 | (int data) => emitted.add(StreamEvent.data(data)), 1412 | onError: (Object error) => emitted.add(StreamEvent.error(error)), 1413 | ); 1414 | await pumpEventQueue(); 1415 | await listener.cancel(); 1416 | expect(emitted, isEmpty); 1417 | } 1418 | 1419 | /// Returns a stream transformer that retains only even integers. 1420 | StreamTransformer whereEven() => StreamTransformer.fromBind( 1421 | (stream) => stream.where((value) => value % 2 == 0), 1422 | ); 1423 | 1424 | void expectCannotBeWiredAgain(Node node) { 1425 | expect(() => node.wireAsPassthrough(Stream.empty()), throwsStateError); 1426 | expect(() => node.wire0(), throwsStateError); 1427 | final node2 = Node()..wire0(); 1428 | expect(() => node.wire1(node2, (a) => a), throwsStateError); 1429 | expect( 1430 | () => node.wire2(node2, node2, (a, b) => a + b), 1431 | throwsStateError, 1432 | ); 1433 | expect( 1434 | () => node.wire3( 1435 | node2, 1436 | node2, 1437 | node2, 1438 | (a, b, c) => a + b + c, 1439 | ), 1440 | throwsStateError, 1441 | ); 1442 | expect( 1443 | () => node.wire4( 1444 | node2, 1445 | node2, 1446 | node2, 1447 | node2, 1448 | (a, b, c, d) => a + b + c + d, 1449 | ), 1450 | throwsStateError, 1451 | ); 1452 | expect( 1453 | () => node.wireAsTransformedNode(node2, whereEven()), 1454 | throwsStateError, 1455 | ); 1456 | expect( 1457 | () => node.wireAsTransformedStream(node2.stream, whereEven()), 1458 | throwsStateError, 1459 | ); 1460 | } 1461 | 1462 | /// Extension to provide convenience methods on [List]. 1463 | extension ListConveniences on List { 1464 | // Returns a sublist of the last N items in the list. 1465 | List lastItems(int n) { 1466 | return sublist(length - n); 1467 | } 1468 | } 1469 | -------------------------------------------------------------------------------- /test/ordered_executor_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:mockito/annotations.dart'; 16 | import 'package:mockito/mockito.dart'; 17 | import 'package:flutter_stream_extensions/ordered_executor.dart'; 18 | import 'package:test/test.dart'; 19 | 20 | import 'ordered_executor_test.mocks.dart'; 21 | 22 | @GenerateNiceMocks([MockSpec()]) 23 | void main() { 24 | group(OrderedExecutor, () { 25 | late OrderedExecutor executor; 26 | late MockCallback callback; 27 | 28 | setUp(() { 29 | callback = MockCallback(); 30 | }); 31 | 32 | tearDown(() { 33 | executor.dispose(); 34 | }); 35 | 36 | group('after disposal', () { 37 | setUp(() { 38 | executor = OrderedExecutor(callback)..dispose(); 39 | }); 40 | 41 | test('throws from run()', () { 42 | expect(() => executor.run(123), throwsStateError); 43 | verifyNever(callback.call(any)); 44 | }); 45 | }); 46 | 47 | group('with no prior param', () { 48 | setUp(() { 49 | executor = OrderedExecutor(callback); 50 | }); 51 | 52 | test('returns done with param and valid result', () { 53 | when(callback.call(123)).thenAnswer((_) async => 'value123'); 54 | expect(executor.run(123), completion(ExecutionStatus.done)); 55 | expect(executor.result.stream, emits('value123')); 56 | verify(callback.call(123)); 57 | verifyNoMoreInteractions(callback); 58 | }); 59 | 60 | test('returns done with param and error', () { 61 | when(callback.call(-456)).thenAnswer((_) async { 62 | throw 'error-456'; 63 | }); 64 | expect(executor.run(-456), completion(ExecutionStatus.done)); 65 | expect(executor.result.stream, emitsError('error-456')); 66 | verify(callback.call(-456)); 67 | verifyNoMoreInteractions(callback); 68 | }); 69 | 70 | test('returns aborted when the execution completes after disposal', () { 71 | expect(executor.run(123), completion(ExecutionStatus.aborted)); 72 | executor.dispose(); 73 | verify(callback.call(123)); 74 | verifyNoMoreInteractions(callback); 75 | }); 76 | }); 77 | 78 | group('with pending prior param', () { 79 | setUp(() { 80 | when(callback.call(123)).thenAnswer((_) async => 'value123'); 81 | executor = OrderedExecutor(callback); 82 | }); 83 | 84 | test('does not start a new execution from run() with the same param', () { 85 | final prior = executor.run(123); 86 | final current = executor.run(123); 87 | expect(prior, completion(ExecutionStatus.done)); 88 | expect(current, completion(ExecutionStatus.done)); 89 | expect(executor.result.stream, emits('value123')); 90 | verify(callback.call(123)); 91 | verifyNoMoreInteractions(callback); 92 | }); 93 | 94 | test('starts a new execution from run() with a different param', () { 95 | when(callback.call(456)).thenAnswer((_) async => 'value456'); 96 | final prior = executor.run(123); 97 | final current = executor.run(456); 98 | expect(prior, completion(ExecutionStatus.preempted)); 99 | expect(current, completion(ExecutionStatus.done)); 100 | expect(executor.result.stream, emits('value456')); 101 | verify(callback.call(123)); 102 | verify(callback.call(456)); 103 | verifyNoMoreInteractions(callback); 104 | }); 105 | }); 106 | 107 | group('with completed prior param', () { 108 | setUp(() { 109 | when(callback.call(123)).thenAnswer((_) async => 'value123'); 110 | executor = OrderedExecutor(callback); 111 | }); 112 | 113 | test('starts a new execution with the same param', () async { 114 | final prior = executor.run(123); 115 | await pumpEventQueue(); 116 | final current = executor.run(123); 117 | expect(prior, completion(ExecutionStatus.done)); 118 | expect(current, completion(ExecutionStatus.done)); 119 | expect( 120 | executor.result.stream, 121 | emitsInOrder([ 122 | 'value123', 123 | 'value123', 124 | ]), 125 | ); 126 | verify(callback.call(123)).called(2); 127 | verifyNoMoreInteractions(callback); 128 | }); 129 | 130 | test('starts a new execution with a different param', () async { 131 | when(callback.call(456)).thenAnswer((_) async => 'value456'); 132 | final prior = executor.run(123); 133 | await pumpEventQueue(); 134 | final current = executor.run(456); 135 | expect(prior, completion(ExecutionStatus.done)); 136 | expect(current, completion(ExecutionStatus.done)); 137 | expect( 138 | executor.result.stream, 139 | emitsInOrder([ 140 | 'value123', 141 | 'value456', 142 | ]), 143 | ); 144 | verify(callback.call(123)); 145 | verify(callback.call(456)); 146 | verifyNoMoreInteractions(callback); 147 | }); 148 | }); 149 | }); 150 | } 151 | 152 | abstract class Callback { 153 | Future call(int value); 154 | } 155 | -------------------------------------------------------------------------------- /test/sequential_executor_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:flutter_stream_extensions/sequential_executor.dart'; 18 | import 'package:quiver/check.dart'; 19 | import 'package:test/test.dart'; 20 | 21 | void main() { 22 | group(SequentialExecutor, () { 23 | late SequentialExecutor executor; 24 | late Task intTask; 25 | late Task stringTask; 26 | late Task doubleTask; 27 | 28 | setUp(() { 29 | executor = SequentialExecutor(); 30 | intTask = Task(); 31 | stringTask = Task(); 32 | doubleTask = Task(); 33 | }); 34 | 35 | tearDown(() { 36 | if (!intTask.isCompleted) { 37 | intTask.complete(null); 38 | } 39 | if (!stringTask.isCompleted) { 40 | stringTask.complete(null); 41 | } 42 | if (!doubleTask.isCompleted) { 43 | doubleTask.complete(null); 44 | } 45 | }); 46 | 47 | group('with multiple zones', () { 48 | late Zone siblingA; 49 | late Zone siblingB; 50 | late Zone childA; 51 | 52 | setUp(() { 53 | // Create a tree of zones: 54 | // ``` 55 | // [current] 56 | // / \ 57 | // [siblingA] [siblingB] 58 | // | 59 | // [childA] 60 | // ``` 61 | siblingA = Zone.current.fork(); 62 | siblingB = Zone.current.fork(); 63 | childA = siblingA.fork(); 64 | }); 65 | 66 | test('can run tasks from sibling zones', () async { 67 | siblingA.run(() { 68 | expect(executor.run(intTask), completion(123)); 69 | }); 70 | siblingB.run(() { 71 | expect(executor.run(stringTask), completion('abc')); 72 | }); 73 | expect(intTask.called, isTrue); 74 | expect(stringTask.called, isFalse); 75 | siblingA.run(() { 76 | intTask.complete(123); 77 | }); 78 | await pumpEventQueue(); 79 | expect(stringTask.called, isTrue); 80 | siblingB.run(() { 81 | stringTask.complete('abc'); 82 | }); 83 | }); 84 | 85 | test('can run tasks from the zone and its child zone', () async { 86 | siblingA.run(() { 87 | expect(executor.run(intTask), completion(123)); 88 | }); 89 | childA.run(() { 90 | expect(executor.run(stringTask), completion('abc')); 91 | }); 92 | expect(intTask.called, isTrue); 93 | expect(stringTask.called, isFalse); 94 | siblingA.run(() { 95 | intTask.complete(123); 96 | }); 97 | await pumpEventQueue(); 98 | expect(stringTask.called, isTrue); 99 | childA.run(() { 100 | stringTask.complete('abc'); 101 | }); 102 | }); 103 | 104 | test('is not affected by errors from a sibling-zoned task', () async { 105 | siblingA.run(() { 106 | expect(executor.run(intTask), throwsStateError); 107 | }); 108 | siblingB.run(() { 109 | expect(executor.run(stringTask), completion('abc')); 110 | }); 111 | expect(intTask.called, isTrue); 112 | expect(stringTask.called, isFalse); 113 | siblingA.run(() { 114 | intTask.completeError(StateError('fatal')); 115 | }); 116 | await pumpEventQueue(); 117 | expect(stringTask.called, isTrue); 118 | siblingB.run(() { 119 | stringTask.complete('abc'); 120 | }); 121 | }); 122 | 123 | test('is not affected by errors from a parent-zoned task', () async { 124 | siblingA.run(() { 125 | expect(executor.run(intTask), throwsStateError); 126 | }); 127 | childA.run(() { 128 | expect(executor.run(stringTask), completion('abc')); 129 | }); 130 | expect(intTask.called, isTrue); 131 | expect(stringTask.called, isFalse); 132 | siblingA.run(() { 133 | intTask.completeError(StateError('fatal')); 134 | }); 135 | await pumpEventQueue(); 136 | expect(stringTask.called, isTrue); 137 | childA.run(() { 138 | stringTask.complete('abc'); 139 | }); 140 | }); 141 | 142 | test('is not affected by errors from a child-zoned task', () async { 143 | childA.run(() { 144 | expect(executor.run(intTask), throwsStateError); 145 | }); 146 | siblingA.run(() { 147 | expect(executor.run(stringTask), completion('abc')); 148 | }); 149 | expect(intTask.called, isTrue); 150 | expect(stringTask.called, isFalse); 151 | childA.run(() { 152 | intTask.completeError(StateError('fatal')); 153 | }); 154 | await pumpEventQueue(); 155 | expect(stringTask.called, isTrue); 156 | siblingA.run(() { 157 | stringTask.complete('abc'); 158 | }); 159 | }); 160 | }); 161 | 162 | group('without pending tasks', () { 163 | test('runs immediately', () { 164 | executor.run(intTask); 165 | expect(intTask.called, isTrue); 166 | }); 167 | 168 | test('returns expected value when completed', () { 169 | expect(executor.run(intTask), completion(123)); 170 | intTask.complete(123); 171 | }); 172 | 173 | test('returns expected error when completed', () { 174 | expect(executor.run(intTask), throwsA('big error')); 175 | intTask.completeError('big error'); 176 | }); 177 | }); 178 | 179 | group('with a pending task', () { 180 | setUp(() { 181 | // Make sure to catch all errors here to prevent any throwable from 182 | // going to the Zone error reporter. 183 | executor.run(intTask).catchError((_) {}); 184 | }); 185 | 186 | test('does not run immediately', () { 187 | executor.run(stringTask); 188 | expect(stringTask.called, isFalse); 189 | }); 190 | 191 | group('after predecessor completes with value', () { 192 | late Future stringResult; 193 | 194 | setUp(() async { 195 | stringResult = executor.run(stringTask); 196 | intTask.complete(123); 197 | await pumpEventQueue(); 198 | }); 199 | 200 | test('runs now', () { 201 | expect(stringTask.called, isTrue); 202 | }); 203 | 204 | test('returns expected value when completed', () { 205 | expect(stringResult, completion('abc')); 206 | stringTask.complete('abc'); 207 | }); 208 | 209 | test('returns expected error when completed', () { 210 | expect(stringResult, throwsA('huge error')); 211 | stringTask.completeError('huge error'); 212 | }); 213 | }); 214 | 215 | group('after predecessor completes with error', () { 216 | late Future stringResult; 217 | 218 | setUp(() async { 219 | stringResult = executor.run(stringTask); 220 | intTask.completeError('big error'); 221 | await pumpEventQueue(); 222 | }); 223 | 224 | test('runs now', () { 225 | expect(stringTask.called, isTrue); 226 | }); 227 | 228 | test('returns expected value when completed', () { 229 | expect(stringResult, completion('abc')); 230 | stringTask.complete('abc'); 231 | }); 232 | 233 | test('returns expected error when completed', () { 234 | expect(stringResult, throwsA('huge error')); 235 | stringTask.completeError('huge error'); 236 | }); 237 | }); 238 | }); 239 | 240 | // Verifies the ordering of pending tasks. 241 | group('with two pending tasks', () { 242 | late Future doubleResult; 243 | 244 | setUp(() { 245 | // Make sure to catch all errors here to prevent any throwable from 246 | // going to the Zone error reporter. 247 | executor.run(intTask).catchError((_) {}); 248 | executor.run(stringTask).catchError((_) {}); 249 | doubleResult = executor.run(doubleTask); 250 | }); 251 | 252 | test('does not run immediately', () { 253 | expect(doubleTask.called, isFalse); 254 | }); 255 | 256 | group('and first pending task completes with value', () { 257 | setUp(() async { 258 | intTask.complete(123); 259 | await pumpEventQueue(); 260 | }); 261 | 262 | test('still does not run', () { 263 | expect(doubleTask.called, isFalse); 264 | }); 265 | 266 | group('and second pending task completes with value', () { 267 | setUp(() async { 268 | stringTask.complete('abc'); 269 | await pumpEventQueue(); 270 | }); 271 | 272 | test('runs now', () { 273 | expect(doubleTask.called, isTrue); 274 | }); 275 | 276 | test('returns expected value when completed', () { 277 | expect(doubleResult, completion(1.2)); 278 | doubleTask.complete(1.2); 279 | }); 280 | 281 | test('returns expected error when completed', () { 282 | expect(doubleResult, throwsA('fatal error')); 283 | doubleTask.completeError('fatal error'); 284 | }); 285 | }); 286 | 287 | group('and second pending task completes with error', () { 288 | setUp(() async { 289 | stringTask.completeError('huge error'); 290 | await pumpEventQueue(); 291 | }); 292 | 293 | test('runs now', () { 294 | expect(doubleTask.called, isTrue); 295 | }); 296 | 297 | test('returns expected value when completed', () { 298 | expect(doubleResult, completion(1.2)); 299 | doubleTask.complete(1.2); 300 | }); 301 | 302 | test('returns expected error when completed', () { 303 | expect(doubleResult, throwsA('fatal error')); 304 | doubleTask.completeError('fatal error'); 305 | }); 306 | }); 307 | }); 308 | 309 | group('and first pending task completes with error', () { 310 | setUp(() async { 311 | intTask.completeError('big error'); 312 | await pumpEventQueue(); 313 | }); 314 | 315 | test('still does not run', () { 316 | expect(doubleTask.called, isFalse); 317 | }); 318 | 319 | group('and second pending task completes with value', () { 320 | setUp(() async { 321 | stringTask.complete('abc'); 322 | await pumpEventQueue(); 323 | }); 324 | 325 | test('runs now', () { 326 | expect(doubleTask.called, isTrue); 327 | }); 328 | 329 | test('returns expected value when completed', () { 330 | expect(doubleResult, completion(1.2)); 331 | doubleTask.complete(1.2); 332 | }); 333 | 334 | test('returns expected error when completed', () { 335 | expect(doubleResult, throwsA('fatal error')); 336 | doubleTask.completeError('fatal error'); 337 | }); 338 | }); 339 | 340 | group('and second pending task completes with error', () { 341 | setUp(() async { 342 | stringTask.completeError('huge error'); 343 | await pumpEventQueue(); 344 | }); 345 | 346 | test('runs now', () { 347 | expect(doubleTask.called, isTrue); 348 | }); 349 | 350 | test('returns expected value when completed', () { 351 | expect(doubleResult, completion(1.2)); 352 | doubleTask.complete(1.2); 353 | }); 354 | 355 | test('returns expected error when completed', () { 356 | expect(doubleResult, throwsA('fatal error')); 357 | doubleTask.completeError('fatal error'); 358 | }); 359 | }); 360 | }); 361 | }); 362 | 363 | group('with an already completed task', () { 364 | setUp(() async { 365 | unawaited(executor.run(intTask).catchError((_) {})); 366 | intTask.complete(123); 367 | await pumpEventQueue(); 368 | }); 369 | 370 | test('runs immediately', () { 371 | executor.run(stringTask); 372 | expect(stringTask.called, isTrue); 373 | }); 374 | 375 | test('returns expected value when completed', () { 376 | expect(executor.run(stringTask), completion('abc')); 377 | stringTask.complete('abc'); 378 | }); 379 | 380 | test('returns expected error when completed', () { 381 | expect(executor.run(stringTask), throwsA('huge error')); 382 | stringTask.completeError('huge error'); 383 | }); 384 | }); 385 | }); 386 | } 387 | 388 | /// A fake async task for unit testing. 389 | class Task { 390 | /// Whether this task has been called. 391 | bool get called => _called; 392 | bool _called = false; 393 | 394 | /// Completer of this task's returned future. 395 | final _completer = Completer(); 396 | 397 | /// Whether this task has completed, either with a value or an error. 398 | bool get isCompleted => _completer.isCompleted; 399 | 400 | /// Completes this task with [value]. 401 | void complete(T value) => _completer.complete(value); 402 | 403 | /// Completes this task with [error]. 404 | void completeError(Object error) => _completer.completeError(error); 405 | 406 | /// Calls this task. You don't have to call this method directly - the object 407 | /// itself is a function and can be called directly: 408 | /// 409 | /// ```dart 410 | /// final task = Task(); 411 | /// // Invoke this task. 412 | /// task(); 413 | /// ``` 414 | /// 415 | /// This method prevents reentraincy - it throws [StateError] when called a 416 | /// second time. 417 | Future call() { 418 | checkState(!_called); 419 | _called = true; 420 | return _completer.future; 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /test/widgets_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:flutter/widgets.dart'; 18 | import 'package:flutter_test/flutter_test.dart'; 19 | import 'package:mockito/mockito.dart'; 20 | import 'package:flutter_stream_extensions/widgets.dart'; 21 | 22 | void main() { 23 | group(DistinctStreamBuilder, () { 24 | late MockBuildWithValue builder1; 25 | late MockBuildWithValue builder2; 26 | late Key key; 27 | 28 | setUp(() { 29 | builder1 = MockBuildWithValue(); 30 | builder2 = MockBuildWithValue(); 31 | key = GlobalKey(); 32 | }); 33 | 34 | tearDown(() { 35 | verifyNoMoreInteractions(builder1); 36 | verifyNoMoreInteractions(builder2); 37 | }); 38 | 39 | /// Returns a [DistinctStreamBuilder] with [stream] and [initialValue]. 40 | /// 41 | /// Widgets returned by this method shares the same [key]. 42 | Widget withBuilder( 43 | BuildWithValue builder, 44 | Stream stream, 45 | String initialValue, 46 | ) => 47 | DistinctStreamBuilder( 48 | stream: stream, 49 | initialValue: initialValue, 50 | builder: (context, child, dynamic value) { 51 | builder(value); 52 | return Container(); 53 | }, 54 | key: key, 55 | ); 56 | 57 | testWidgets('builds with initial value', (tester) async { 58 | await tester 59 | .pumpWidget(withBuilder(builder1, Stream.empty(), 'aaa')); 60 | verify(builder1.call('aaa')); 61 | }); 62 | 63 | testWidgets('builds with values from stream', (tester) async { 64 | final controller = StreamController(); 65 | await tester.pumpWidget(withBuilder(builder1, controller.stream, 'aaa')); 66 | controller.add('aaa'); 67 | await tester.pumpAndSettle(); 68 | controller.add('bbb'); 69 | await tester.pumpAndSettle(); 70 | controller.add('bbb'); 71 | await tester.pumpAndSettle(); 72 | controller.add('ccc'); 73 | await tester.pumpAndSettle(); 74 | controller.add(null); 75 | await tester.pumpAndSettle(); 76 | controller.add('ddd'); 77 | await tester.pumpAndSettle(); 78 | verifyInOrder([ 79 | builder1.call('aaa'), 80 | builder1.call('bbb'), 81 | builder1.call('ccc'), 82 | builder1.call(null), 83 | builder1.call('ddd'), 84 | ]); 85 | }); 86 | 87 | group('when only builder changed', () { 88 | testWidgets('builds current value with new builder', (tester) async { 89 | final stream = Stream.fromIterable(['bbb']); 90 | // Note both widgets are built with the same global [key]. 91 | await tester.pumpWidget(withBuilder(builder1, stream, 'aaa')); 92 | await tester.pumpAndSettle(); 93 | await tester.pumpWidget(withBuilder(builder2, stream, 'aaa')); 94 | await tester.pumpAndSettle(); 95 | verifyInOrder([ 96 | builder1.call('aaa'), 97 | builder1.call('bbb'), 98 | builder2.call('bbb'), 99 | ]); 100 | }); 101 | }); 102 | 103 | group('when builder and initial value changed', () { 104 | testWidgets('builds initial value with new builder', (tester) async { 105 | final stream = Stream.fromIterable(['bbb']); 106 | // Note both widgets are built with the same global [key]. 107 | await tester.pumpWidget(withBuilder(builder1, stream, 'aaa')); 108 | await tester.pumpAndSettle(); 109 | await tester.pumpWidget(withBuilder(builder2, stream, 'ccc')); 110 | await tester.pumpAndSettle(); 111 | verifyInOrder([ 112 | builder1.call('aaa'), 113 | builder1.call('bbb'), 114 | builder2.call('ccc'), 115 | ]); 116 | }); 117 | }); 118 | 119 | group('when only initial value changed', () { 120 | testWidgets('builds new initial value', (tester) async { 121 | final stream = Stream.fromIterable(['bbb']); 122 | // Note both widgets are built with the same global [key]. 123 | await tester.pumpWidget(withBuilder(builder1, stream, 'aaa')); 124 | await tester.pumpAndSettle(); 125 | await tester.pumpWidget(withBuilder(builder1, stream, 'bbb')); 126 | await tester.pumpAndSettle(); 127 | verifyInOrder([ 128 | builder1.call('aaa'), 129 | builder1.call('bbb'), 130 | builder1.call('bbb'), 131 | ]); 132 | }); 133 | }); 134 | 135 | group('when only stream changed', () { 136 | testWidgets('resets current value and stream', (tester) async { 137 | // Note both widgets are built with the same global [key]. 138 | await tester.pumpWidget( 139 | withBuilder(builder1, Stream.fromIterable(['bbb']), 'aaa'), 140 | ); 141 | await tester.pumpAndSettle(); 142 | await tester.pumpWidget( 143 | withBuilder(builder1, Stream.fromIterable(['ccc']), 'aaa'), 144 | ); 145 | await tester.pumpAndSettle(); 146 | verifyInOrder([ 147 | builder1.call('aaa'), 148 | builder1.call('bbb'), 149 | builder1.call('aaa'), 150 | builder1.call('ccc'), 151 | ]); 152 | }); 153 | }); 154 | 155 | group('when stream and initial value changed', () { 156 | testWidgets('resets current value and stream', (tester) async { 157 | // Note both widgets are built with the same global [key]. 158 | await tester.pumpWidget( 159 | withBuilder(builder1, Stream.fromIterable(['bbb']), 'aaa'), 160 | ); 161 | await tester.pumpAndSettle(); 162 | await tester.pumpWidget( 163 | withBuilder(builder1, Stream.fromIterable(['ccc']), 'ddd'), 164 | ); 165 | await tester.pumpAndSettle(); 166 | verifyInOrder([ 167 | builder1.call('aaa'), 168 | builder1.call('bbb'), 169 | builder1.call('ddd'), 170 | builder1.call('ccc'), 171 | ]); 172 | }); 173 | }); 174 | 175 | group('when all three changed', () { 176 | testWidgets('resets current value and stream', (tester) async { 177 | // Note both widgets are built with the same global [key]. 178 | await tester.pumpWidget( 179 | withBuilder(builder1, Stream.fromIterable(['bbb']), 'aaa'), 180 | ); 181 | await tester.pumpAndSettle(); 182 | await tester.pumpWidget( 183 | withBuilder(builder2, Stream.fromIterable(['ccc']), 'ddd'), 184 | ); 185 | await tester.pumpAndSettle(); 186 | verifyInOrder([ 187 | builder1.call('aaa'), 188 | builder1.call('bbb'), 189 | builder2.call('ddd'), 190 | builder2.call('ccc'), 191 | ]); 192 | }); 193 | }); 194 | }); 195 | } 196 | 197 | abstract class BuildWithValue { 198 | void call(String? value); 199 | } 200 | 201 | class MockBuildWithValue extends Mock implements BuildWithValue {} 202 | --------------------------------------------------------------------------------