From 339bd3a6f075b097955ae0bf80cadfe1a5f78833 Mon Sep 17 00:00:00 2001
From: Marcelo Glasberg <13332110+marcglasberg@users.noreply.github.com>
Date: Tue, 9 Jul 2024 16:31:37 -0300
Subject: [PATCH] Version bump.
---
CHANGELOG.md | 14 +-
README.md | 4038 +++++---------------------------------------------
pubspec.yaml | 7 +-
3 files changed, 352 insertions(+), 3707 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2d4fc9..fac023a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,13 @@
-_Please visit
-an
-Async Redux App Example Repository in GitHub for a full-fledged example with a complete app
-showcasing the fundamentals and best practices described in the AsyncRedux README.md file._
+_Visit
+the
+Async Redux App Example GitHub Repo for a full-fledged example app showcasing the fundamentals
+and best practices._
+
+# 23.1.0
+
+* New: Async Redux website at https://asyncredux.com
+
+* New: [Async Redux for React](https://www.npmjs.com/package/async-redux-react)
# 23.0.2
diff --git a/README.md b/README.md
index 5ac3214..d4c8346 100644
--- a/README.md
+++ b/README.md
@@ -1,125 +1,210 @@
[![pub package](https://img.shields.io/pub/v/async_redux.svg)](https://pub.dartlang.org/packages/async_redux)
[![pub package](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter)
+
+
# Async Redux | *state management*
-Async Redux is an optimized Redux version, which is very easy to learn and use,
-yet powerful and tailored for Flutter.
-It helps you write Flutter apps that are **easy to test, maintain and extend**.
+* Simple to learn and easy to use
+* Powerful enough to handle complex applications with millions of users
+* Testable
-# Developer Overview
+This means you'll be able to create apps much faster,
+and other people on your team will easily understand and modify your code.
-The main concepts in Redux are: *store*, *state*, *actions* and *reducers*.
+# What is it?
-The **store** holds all the application **state**,
-and you can only change the state by dispatching **actions**.
-Each action has a **reducer**, which changes the state:
+An optimized reimagined version of Redux.
+A mature solution, battle-tested in hundreds of real-world applications.
+Written from the ground up, created by [Marcelo Glasberg](https://github.com/marcglasberg).
+
+> There is also a [version for React](https://www.npmjs.com/package/async-redux-react)
+
+# Documentation
+
+The complete docs are published at **https://asyncredux.com**
+
+Below is a quick overview.
+
+***
+
+# Store, state, actions and reducers
+
+The store holds all the application **state**. A few examples:
```dart
-// Create a store, which holds the initial app state.
+// Here, the state is a number
var store = Store(initialState: 1);
+```
-// Create an action class with a `reduce` method that changes the state.
-class Increment extends Action {
- int reduce() => state + 1;
+```dart
+// Here, the state is an object
+class AppState {
+ final String name;
+ final int age;
+ State(this.name, this.age);
}
-void main() {
- // Dispatch the action to change the state.
- store.dispatch(Increment());
- print(store.state); // 2
-}
+var store = Store(initialState: AppState('Mary', 25));
```
-To use the `store` in your widgets, add a `StoreProvider` to the top of your widget tree:
+
+
+To use the store, add it in a `StoreProvider` at the top of your widget tree.
```dart
-Widget build(BuildContext context) {
+Widget build(context) {
return StoreProvider(
store: store,
- child: MaterialApp(home: MyHomePage()), ...
- );
+ child: MaterialApp( ... ),
+ );
}
```
-You can then use it anywhere in your widgets like this:
+
+
+# Widgets use the state
```dart
-class MyWidget extends StatelessWidget {
- Widget build(BuildContext context) {
- return Column(children: [
-
- Text(context.state.toString()), // Use the state.
-
- ElevatedButton(
- child: Text('+')),
- onPressed: () => context.dispatch(Increment())) // Dispatch the action.
- ]);
- }}
+class MyWidget extends StatelessWidget {
+
+ Widget build(context) {
+ return Text('${context.state.name} has ${context.state.age} years old');
+ }
+}
+```
+
+
+
+# Actions and reducers
+
+An **action** is a class that contain its own **reducer**.
+
+```dart
+class Increment extends Action {
+
+ // The reducer has access to the current state
+ int reduce() => state + 1; // It returns a new state
+}
+```
+
+
+
+# Dispatch an action
+
+The store state is **immutable**.
+
+The only way to change the store **state** is by dispatching an **action**.
+The action reducer returns a new state, that replaces the old one.
+
+```tsx
+// Dispatch an action
+store.dispatch(Increment());
+
+// Dispatch multiple actions
+store.dispatchAll([Increment(), LoadText()]);
+
+// Dispatch an action and wait for it to finish
+await store.dispatchAndWait(Increment());
+
+// Dispatch multiple actions and wait for them to finish
+await store.dispatchAndWaitAll([Increment(), LoadText()]);
+```
+
+
+
+# Widgets can dispatch actions
+
+The context extensions to dispatch actions are `dispatch` , `dispatchAll` etc.
+
+```dart
+class MyWidget extends StatelessWidget {
+
+ Widget build(context) {
+ return ElevatedButton(
+ onPressed: () => context.dispatch(Increment());
+ }
+}
```
-Your actions can download information from the internet, or do any other asynchronous work:
+
+
+# Actions can do asynchronous work
+
+They download information from the internet, or do any other async work.
```dart
var store = Store(initialState: '');
+```
+```dart
class LoadText extends Action {
- // This reducer returns a Future.
+ // This reducer returns a Future
Future reduce() async {
- // Download some information from the internet.
- var response = await http.get('http://numbersapi.com/42');
+ // Download something from the internet
+ var response = await http.get('https://dummyjson.com/todos/1');
- // Change the state with the downloaded information.
+ // Change the state with the downloaded information
return response.body;
}
}
```
-If some error happens, you can simply throw an `UserException`.
-A dialog (or other UI) will open automatically, showing the error message to the user.
+
-```dart
-var store = Store(initialState: '');
+> If you want to understand the above code in terms of traditional Redux patterns,
+> all code until the last `await` in the `reduce` method is the equivalent of a middleware,
+> and all code after that is the equivalent of a traditional reducer.
+> It's still Redux, just written in a way that is easy and boilerplate-free.
+> No need for Thunks or Sagas.
+
+
+
+# Actions can throw errors
+
+If something bad happens, you can simply **throw an error**. In this case, the state will not
+change. Errors are caught globally and can be handled in a central place, later.
+In special, if you throw a `UserException`, which is a type provided by Async Redux,
+a dialog (or other UI) will open automatically, showing the error message to the user.
+
+```dart
class LoadText extends Action {
Future reduce() async {
- var response = await http.get('http://numbersapi.com/42');
+ var response = await http.get('https://dummyjson.com/todos/1');
if (response.statusCode == 200) return response.body;
- else throw UserException('Failed to load data');
+ else throw UserException('Failed to load');
}
}
```
-Then:
+
-* If you want to show a spinner while the text is loading, you can use `isWaiting`.
-* If you want to show an error message as part of your widget tree, you can use `isFailed`.
+To show a spinner while an asynchronous action is running, use `isWaiting(action)`.
+
+To show an error message inside the widget, use `isFailed(action)`.
```dart
class MyWidget extends StatelessWidget {
- Widget build(BuildContext context) {
-
- if (context.isWaiting(LoadText)) return CircularProgressIndicator();
+
+ Widget build(context) {
+ if (context.isWaiting(LoadText)) return CircularProgressIndicator();
if (context.isFailed(LoadText)) return Text('Loading failed...');
-
- return Column(children: [
-
- Text(context.state), // Show the state
-
- ElevatedButton(
- child: Text('Load')),
- onPressed: () => context.dispatch(LoadText())) // Dispatch the action.
- ]);
-}}
+ return Text(context.state);
+ }
+}
```
-Your actions can also dispatch other actions, and use `dispatchAndWait` to
-wait for an action to finish:
+
+
+# Actions can dispatch other actions
+
+You can use `dispatchAndWait` to dispatch an action and wait for it to finish.
```dart
class LoadTextAndIncrement extends Action {
@@ -135,9 +220,11 @@ class LoadTextAndIncrement extends Action {
}
```
-You can also dispatch actions in parallel and wait for them to finish:
+
+
+You can also dispatch actions in **parallel** and wait for them to finish:
-```dart
+```dart
class BuyAndSell extends Action {
Future reduce() async {
@@ -151,9 +238,11 @@ class BuyAndSell extends Action {
return state.copy(message: 'New cash balance is ${state.cash}');
}
}
-```
+```
-You can also use waitCondition to wait until the state changes in a certain way:
+
+
+You can also use `waitCondition` to wait until the `state` changes in a certain way:
```dart
class SellStockForPrice extends Action {
@@ -168,3748 +257,295 @@ class SellStockForPrice extends Action {
(state) => state.stocks[stock].price >= limitPrice
);
- dispatch(SellStock(stock));
+ // Only then, post the sell order to the backend
+ var amount = await postSellOrder(stock);
- // No further state change
- return null;
+ return state.copy(
+ stocks: state.stocks.setAmount(stock, amount),
+ );
}
```
-You can add **mixins** to your actions, to accomplish common tasks:
-
-* `CheckInternet` ensures actions only run with internet, otherwise an error dialog
- prompts users to check their connection:
-
- ```dart
- class LoadText extends Action with CheckInternet {
-
- Future reduce() async {
- var response = await http.get('http://numbersapi.com/42');
- ...
- }}
- ```
-
-* `NoDialog` can be added to `CheckInternet` so that no dialog is opened.
- Instead, you can display some information in your widgets:
-
- ```dart
- class LoadText extends Action with CheckInternet, NoDialog { ... }
-
- if (context.isFailed(LoadText)) Text('No Internet connection');
- ```
-
-* `AbortWhenNoInternet` aborts the action silently (without showing any dialogs)
- if there is no internet connection.
+
-* `NonReentrant` prevents reentrant actions, so that when you dispatch an action that's
- already running it gets aborted.
+# Add features to your actions
-* `Retry` retries the action a few times with exponential backoff, if it fails.
- Add `UnlimitedRetries` to retry the action indefinitely:
+You can add **mixins** to your actions, to accomplish common tasks.
- ```dart
- class LoadText extends ReduxAction with Retry, UnlimitedRetries, NonReentrant {
- ```
+## Check for Internet connectivity
-Testing your app is very easy. Just dispatch actions and wait for them to finish.
-Then, verify the new state or check if some error was thrown:
+`CheckInternet` ensures actions only run with internet,
+otherwise an **error dialog** prompts users to check their connection:
```dart
-class AppState {
- User user;
- int selected;
- List items;
-}
-
-test('Selecting an item', () async {
-
- var store = Store(
- initialState: AppState(
- user: User(name: 'John'),
- selected: -1,
- items: [Item(id: 1), Item(id: 2), Item(id: 3)]
- ));
-
- // Found item 2.
- await store.dispatchAndWait(SelectItem(2));
- expect(store.state.selected, 2);
-
- // Failed to find item 42.
- var status = await store.dispatchAndWait(SelectItem(42));
- expect(status.originalError, isA<>(UserException));
-});
+class LoadText extends Action with CheckInternet {
+
+ Future reduce() async {
+ var response = await http.get('https://dummyjson.com/todos/1');
+ ...
+ }
+}
```
-# Team Lead Overview
-
-If you are a Team Lead, you'll have features to help you set up the app's infrastructure in a
-central place, and allow your developers to concentrate solely on the business logic.
+
-When you create the store, you can add
-a `persistor` to save and load the state from the local device disk,
-a `stateObserver` to collect app metrics,
-an `errorObserver` to log errors,
-an `actionObserver` to print information to the console during development,
-and a `globalWrapError` to catch all errors thrown by actions and decide what to do with them.
+`NoDialog` can be added to `CheckInternet` so that no dialog is opened.
+Instead, you can display some information in your widgets:
```dart
-var store = Store(
- initialState: '',
- persistor: MyPersistor(),
- stateObserver: [MyStateObserver()],
- errorObserver: [MyErrorObserver()],
- actionObservers: [MyActionObserver()],
- globalWrapError: MyGlobalWrapError(),
-```
-
-For example, the following `GlobalWrapError` is designed to handle all `PlatformException` errors
-throw by **Firebase**. It converts them into `UserException` errors, which are built-in Async Redux
-types that automatically display their message to the user in an error dialog:
+class LoadText extends Action with CheckInternet, NoDialog {
+ ...
+ }
-```dart
-Object? wrap(error, stackTrace, action) {
- return (error is PlatformException)
- ? UserException('Error connecting to Firebase')
- : error;
+class MyWidget extends StatelessWidget {
+ Widget build(context) {
+ if (context.isFailed(LoadText)) Text('No Internet connection');
+ }
}
```
-Another interesting feature for Team Leads is the ability to create a base action class that all
-your actions will extend, and add some common functionality to it. For example, you can add getters
-for the important parts of your state, and also "selectors" to help you find more complex
-information:
-
-```dart
-class AppState {
- User user;
- int selected;
- List items;
-}
+
-class Action extends ReduxAction {
+`AbortWhenNoInternet` aborts the action silently (without showing any dialogs) if there is no
+internet connection.
- // Getters
- User get user => state.user;
- Item get selected => state.selected;
- List get items => state.items;
-
- // Selectors
- Item? findItemById(int id) => items.firstWhereOrNull((item) => item.id == id);
- Item? searchItemByText(String text) => items.firstWhereOrNull((item) => item.text.contains(text));
- int get selectedItemIndex => items.indexOf(selected);
-}
-```
+
-Now, all your actions can use these getters and selectors to access the state in their reducers:
+## NonReentrant
-```dart
-class SelectItem extends Action {
- final int id;
- SelectItem(this.id);
-
- AppState reduce() {
- Item? item = findItemById(id);
- if (item == null) throw UserException('Item not found');
- return state.copy(selected: item);
- }
-}
-```
-
-
-
----
-
----
-
-[//]: # (The below documentation is very detailed. For an overview, go to)
-
-[//]: # (the )
-
-[//]: # (Medium story.)
-
-# Example projects
-
-Please visit:
-
-* **Fully documented:**
- > The Same App
- Different Tech
- Project is a repository in GitHub, containing the same mobile app implemented using a variety
- of different tech stacks, including
- a
- Redux App Example.
-
-* **Documented in the source code only:**
- > The Redux App Example
- repository in GitHub also contains a full-fledged example with a complete app showcasing the
- fundamentals and best practices described in this Readme.
-
-# Table of Contents
-
-* [What is Redux?](#what-is-redux)
-* [Why use this Redux version over others?](#why-use-this-redux-version-over-others)
-* [Store and State](#store-and-state)
-* [Actions](#actions)
- * [Sync Reducer](#sync-reducer)
- * [Async Reducer](#async-reducer)
- * [Changing state is optional](#changing-state-is-optional)
- * [Before and After the Reducer](#before-and-after-the-reducer)
-* [Connector](#connector)
- * [How to provide the ViewModel to the StoreConnector](#how-to-provide-the-viewmodel-to-the-storeconnector)
-* [Alternatives to the Connector](#alternatives-to-the-connector)
- * [Provider](#provider)
-* [Processing errors thrown by Actions](#processing-errors-thrown-by-actions)
- * [Giving better error messages](#giving-better-error-messages)
- * [User exceptions](#user-exceptions)
- * [Converting third-party errors into UserExceptions](#converting-third-party-errors-into-userexceptions)
- * [UserExceptionAction](#userexceptionaction)
-* [Testing](#testing)
- * [Mocking actions and reducers](#mocking-actions-and-reducers)
- * [Testing UserExceptions](#testing-userexceptions)
- * [Test files](#test-files)
-* [Route Navigation](#route-navigation)
-* [Events](#events)
- * [Can I put mutable events into the store state?](#can-i-put-mutable-events-into-the-store-state)
- * [When should I use events?](#when-should-i-use-events)
- * [Advanced event features](#advanced-event-features)
-* [Progress indicators](#progress-indicators)
-* [Waiting until an Action is finished](#waiting-until-an-action-is-finished)
-* [Waiting until the state meets a certain condition](#waiting-until-the-state-meets-a-certain-condition)
-* [State Declaration](#state-declaration)
- * [Selectors](#selectors)
- * [Cache (Reselectors)](#cache-reselectors)
-* [Action Subclassing](#action-subclassing)
- * [Abstract Before and After](#abstract-before-and-after)
-* [Dependency Injection](#dependency-injection)
-* [IDE Navigation](#ide-navigation)
-* [Persistence](#persistence)
- * [Saving and Loading](#saving-and-loading)
-* [Logging](#logging)
-* [Observing rebuilds](#observing-rebuilds)
-* [How to interact with the database](#how-to-interact-with-the-database)
-* [How to deal with Streams](#how-to-deal-with-streams)
- * [So, how do you use streams?](#so-how-do-you-use-streams)
- * [Where the stream subscriptions themselves are stored](#where-the-stream-subscriptions-themselves-are-stored)
- * [How do streams pass their information to the store and ultimately to the widgets?](#how-do-streams-pass-their-information-to-the-store-and-ultimately-to-the-widgets)
- * [To sum up:](#to-sum-up)
-* [Undo and Redo](#undo-and-redo)
-* [Recommended Directory Structure](#recommended-directory-structure)
-* [Where to put your business logic](#where-to-put-your-business-logic)
-* [Architectural discussion](#architectural-discussion)
- * [Is AsyncRedux really Redux?](#is-asyncredux-really-redux)
- * [Besides the reduction of boilerplate, what are the main advantages of the AsyncRedux architecture?](#besides-the-reduction-of-boilerplate-what-are-the-main-advantages-of-the-asyncredux-architecture)
- * [Is AsyncRedux a minimalist or lightweight Redux version?](#is-asyncredux-a-minimalist-or-lightweight-redux-version)
- * [Is the AsyncRedux architecture useful for small projects?](#is-the-asyncredux-architecture-useful-for-small-projects)
-
-
-
-## What is Redux?
-
-A single **store** object holds all the **state**, which is immutable. When you need to modify some
-state, you **dispatch** an **action**. Then a **reducer** creates a new copy of the state, with the
-desired changes. Your widgets are **connected** to the store (through **store-connectors** and
-**view-models**), so they know that the state changed, and rebuild as needed.
-
-
-
-## Why use this Redux version over others?
-
-Plain vanilla Redux is too low-level, which makes it very flexible but results in a lot of
-boilerplate, and a steep learning curve. Combining reducers is a manual task, and you have to list
-them one by one. If you forget to list some reducer, you will not know it until your tests point out
-that some state is not changing as you expected.
-
-Reducers can't be async, so you need to create middleware, which is also difficult to set up and
-use. You have to list them one by one, and if you forget one of them you will also not know it until
-your tests point it out. The `redux_thunk` package can help with that, but adds some more
-complexity.
-
-It's difficult to know which actions fire which reducers, and hard to navigate the code in the IDE.
-In IntelliJ, you may press CTRL+B to navigate between a method use and its declaration. However,
-this is of no use if actions and reducers are independent classes. You have to search for action
-"usages", which is not so convenient since it also list dispatches.
-
-It's also difficult to list all actions and reducers, and you may end up implementing some reducer
-just to realize it already exists with another name.
-
-Testing reducers is simple, since they are pure functions, but integration tests are difficult. In
-the real world you need to test complex middleware that fires other middleware and many reducers,
-with intermediate state changes that you want to test for. Especially if you are doing BDD or
-Acceptance Tests you may need to wait for some middleware to finish, and then dispatch some other
-actions, and test for intermediate states.
-
-Another problem is that vanilla Redux assumes it holds all the application state, and this is not
-practical in a real Flutter app. If you add a simple `TextField` with a `TextEditingController`, or
-a `ListView` with a `ScrollController`, then you have state outside the Redux store. Suppose your
-middleware is downloading some information, and it wishes to scroll a `ListView` as soon as the info
-arrives. This would be simple if the list scroll position is saved in the Redux store. However, this
-state must be in the `ScrollController`, not the store.
-
-**AsyncRedux solves all of these problems and more:**
-
-* It's much easier to learn and use than regular Redux.
-* It comes with its own testing tools that make even complex tests easy to set up and run.
-* You can navigate between action dispatches and their corresponding reducers with a single IDE
- command or click.
-* You can also use your IDE to list all actions/reducers.
-* You don't need to add or list reducers and middleware anywhere.
-* In fact, reducers can be async, so you don't need middleware.
-* There is no need for generated code (as some Redux versions do).
-* It has the concept of "events", to deal with Flutter state controllers.
-* It helps you show errors thrown by reducers to the user.
-* It's easy to add both logging and store persistence.
-
-
-
-## Store and State
-
-Declare your store and state, like this:
+To prevent an action from being dispatched while it's already running,
+add the `NonReentrant` mixin to your action class.
```dart
-var state = AppState.initialState();
-
-var store = Store(
- initialState: state,
-);
-```
-
-Note: _Your state can be any **immutable** object, but typically you create a class
-called `AppState` to help with the state creation and manipulation. I later give
-some [recommendations](#state-declaration) on how to create this class. In special, you can use the
-fast_immutable_collections package
-when you need immutable lists, sets, maps and multimaps._
-
-
-
-
-## Actions
-
-If you want to change the store state you must "dispatch" some action. In AsyncRedux all actions
-extend `ReduxAction`.
-
-The reducer of an action is simply a method of the action itself, called `reduce()`. All actions
-must override this method.
-
-The reducer has direct access to:
-
-- The store state (which is a getter of the `Action` class).
-- The action state itself (the class fields, passed to the action when it was instantiated and
- dispatched).
-- The `dispatch` method, so that other actions may be dispatched from the reducer.
-
-
-
-The abstract `reduce()` method signature has a return type of `FutureOr`, but
-your concrete reducers must return one or the other: `AppState?` or `Future`.
-
-That's necessary because AsyncRedux knows if a reducer is sync or async by checking your `reduce()`
-method signature. If it is `FutureOr`, it can't know if it's sync or async,
-and will throw a `StoreException`:
-
-```
-Reducer should return `St?` or `Future`. Do not return `FutureOr`.
+class LoadText extends Action with NonReentrant {
+ ...
+ }
```
-### Sync Reducer
-
-If you want to do some synchronous work, simply declare the reducer to return `AppState?`, then
-change the state and return it.
-
-For example, let's start with a simple action to increment a counter by some value:
-
-```dart
-class IncrementAction extends ReduxAction {
-
- final int amount;
-
- IncrementAction({this.amount});
+
- @override
- AppState? reduce() {
- return state.copy(counter: state.counter + amount));
- }
-}
-```
+## Retry
-This action is dispatched like this:
+Add `Retry` to retry the action a few times with exponential backoff, if it fails.
+Add `UnlimitedRetries` to retry indefinitely:
```dart
-store.dispatch(IncrementAction(amount: 3));
+class LoadText extends Action with Retry, UnlimitedRetries {
+ ...
+ }
```
-Note the reducer above has direct access to both the counter state (`state.counter`)
-and to the action state (the field `amount`).
-
-We will show you later how to easily test sync reducers, using the **StoreTester**.
+
-Try running
-the:
-Increment Example.
+## Debounce (soon)
-
-
-### Async Reducer
-
-If you want to do some asynchronous work, simply declare the reducer to return `Future`
-then change the state and return it. There is no need of any "middleware", like for other Redux
-versions.
-
-Note: In IntelliJ, to convert the reducer from sync to async, press `Alt+ENTER` and
-select `Convert to async function body`.
-
-As an example, suppose you want to increment a counter by a value you get from the database. The
-database access is async, so you must use an async reducer:
+To limit how often an action occurs in response to rapid inputs, you can add the `Debounce` mixin
+to your action class. For example, when a user types in a search bar, debouncing ensures that not
+every keystroke triggers a server request. Instead, it waits until the user pauses typing before
+acting.
```dart
-class QueryAndIncrementAction extends ReduxAction {
+class SearchText extends Action with Debounce {
+ final String searchTerm;
+ SearchText(this.searchTerm);
+
+ final int debounce = 350; // Milliseconds
- @override
- Future reduce() async {
- int value = await getAmount();
- return state.copy(counter: state.counter + value));
+ Future reduce() async {
+
+ var response = await http.get(
+ Uri.parse('https://example.com/?q=' + encoded(searchTerm))
+ );
+
+ return state.copy(searchResult: response.body);
}
}
```
-This action is dispatched like this:
-
-```dart
-store.dispatch(QueryAndIncrementAction());
-```
-
-Please note: While the `reduce()` method of a *sync* reducer runs synchronously with the dispatch,
-the `reduce()` method of an *async* reducer will be called synchronously, but will always return
-the state in a later microtask.
-
-We will show you later how to easily test async reducers, using the **StoreTester**.
-
-Try running
-the:
-Increment Async Example.
-
-#### One important rule
+
-When your reducer is async (i.e., returns `Future`) you must make sure you **do not return
-a completed future**, meaning all execution paths of the reducer must pass through at least
-one `await` keyword. In other words, don't return a Future if you don't need it.
+## Throttle (soon)
-If you don't follow this rule, AsyncRedux may seem to work ok, but will eventually misbehave.
+To prevent an action from running too frequently, you can add the `Throttle` mixin to your
+action class. This means that once the action runs it's considered _fresh_, and it won't run
+again for a set period of time, even if you try to dispatch it.
+After this period ends, the action is considered _stale_ and is ready to run again.
-If your reducer has no `await`s, you must return `AppState?` instead of `Future`,
-or simply add `await microtask;` to the start of your reducer, or return `null`. For example:
-
-```dart
-// These are right:
-AppState? reduce() {
- return state;
-}
-
-AppState? reduce() {
- someFunc();
- return state;
-}
-
-Future reduce() async {
- await someFuture();
- return state;
-}
+```tsx
+class LoadPrices extends Action with Throttle {
+
+ final int throttle = 5000; // Milliseconds
-Future reduce() async {
- await microtask;
- return state;
+ Future reduce() async {
+ var result = await loadJson('https://example.com/prices');
+ return state.copy(prices: result);
+ }
}
+```
-Future reduce() async {
- if (state.someBool) return await calculation();
- return null;
-}
+
-// But these are wrong:
-Future reduce() async {
- return state;
-}
+## OptimisticUpdate (soon)
-Future reduce() async {
- someFunc();
- return state;
-}
+To provide instant feedback on actions that save information to the server, this feature immediately
+applies state changes as if they were already successful, before confirming with the server.
+If the server update fails, the change is rolled back and, optionally, a notification can inform
+the user of the issue.
-Future reduce() async {
- if (state.someBool) return await calculation();
- return state;
+```tsx
+class SaveName extends Action with OptimisticUpdate {
+
+ async reduce() { ... }
}
```
-It's generally easy to make sure you are not returning a completed future.
-In the rare case your reducer function is very complex, and you are unsure that all code paths
-pass through an `await`, just add `assertUncompletedFuture();` at the very END of your `reduce`
-method, right before the `return`. If you do that, an error will be shown in the console if
-the `reduce` method ever returns a completed future.
-
-If you're an advanced user interested in the details, check the
-
-sync/async tests.
-
-
-
-### Changing state is optional
+
-For both sync and async reducers, returning a new state is optional. If you don't plan on changing
-the state, simply return `null`. This is the same as returning the state unchanged.
+# Events
-Why is this useful? Because some actions may simply start other async processes, or dispatch other
-actions.
-
-For example, suppose you want to have two separate actions, one for querying some value from the
-database, and another action to change the state:
+Flutter widgets like `TextField` and `ListView` hold their own internal state.
+You can use `Events` to interact with them.
```dart
-class QueryAction extends ReduxAction {
-
- @override
- Future reduce() async {
- int value = await getAmount();
- dispatch(IncrementAction(amount: value));
- return null;
+// Action that changes the text of a TextField
+class ChangeText extends Action {
+ final String newText;
+ ChangeText(this.newText);
+
+ AppState reduce() => state.copy(changeText: Event(newText));
}
}
-class IncrementAction extends ReduxAction {
-
- final int amount;
-
- IncrementAction({this.amount});
-
- @override
- AppState reduce() {
- return state.copy(counter: state.counter + amount));
+// Action that scrolls a ListView to the top
+class ScrollToTop extends Action {
+ AppState reduce() => state.copy(scroll: Event(0));
}
}
```
-Note the `reduce()` methods have direct access to `state` and `dispatch`. There is no need to
-write `store.state` and `store.dispatch` (although you can, if you want).
+
-
+# Persist the state
-### Before and After the Reducer
-
-Sometimes, while an async reducer is running, you want to prevent the user from touching the screen.
-Also, sometimes you want to check preconditions like the presence of an internet connection, and
-don't run the reducer if those preconditions are not met.
-
-To help you with these use cases, you may override methods `ReduxAction.before()`
-and `ReduxAction.after()`, which run respectively before and after the reducer.
-
-The `before()` method runs before the reducer. If you want it to run synchronously, it should
-return `void`:
+You can add a `persistor` to save the state to the local device disk.
```dart
-void before() { ... }
+var store = Store(
+ persistor: MyPersistor(),
+);
```
-To run it asynchronously, return `Future`:
+
-```dart
-Future before() async { ... }
-```
+# Testing your app is easy
-If it throws an error, then `reduce()` will NOT run. This means you can use it to check any
-preconditions and throw an error if you want to prevent the reducer from running. For example:
+Just dispatch actions and wait for them to finish.
+Then, verify the new state or check if some error was thrown.
```dart
-Future before() async => await checkInternetConnection();
-```
+class AppState {
+ List items;
+ int selectedItem;
+}
-This method is also capable of dispatching actions, so it can be used to turn on a modal barrier:
+test('Selecting an item', () async {
-```dart
-void before() => dispatch(BarrierAction(true));
+ var store = Store(
+ initialState: AppState(
+ items: ['A', 'B', 'C']
+ selectedItem: -1, // No item selected
+ ));
+
+ // Should select item 2
+ await store.dispatchAndWait(SelectItem(2));
+ expect(store.state.selectedItem, 'B');
+
+ // Fail to select item 42
+ var status = await store.dispatchAndWait(SelectItem(42));
+ expect(status.originalError, isA<>(UserException));
+});
```
-Note: If this method runs asynchronously, then `reduce()` will also be async, since it must wait for
-this one to finish.
-
-The `after()` method runs after `reduce()`, even if an error was thrown by `before()` or `reduce()`
-(akin to a "finally" block).
+
-Avoid `after()` methods which can throw errors. If the `after()` method throws an error, then this
-error will be thrown *asynchronously* (after the "asynchronous gap")
-so that it doesn't interfere with the action. Also, this error will be missing the original
-stacktrace.
+# Advanced setup
-The `after()` method can also dispatch actions, so it can be used to turn off some modal barrier
-when the reducer ends, even if there was some error in the process:
-
-```dart
-void after() => dispatch(BarrierAction(false));
-```
+If you are the Team Lead, you set up the app's infrastructure in a central place,
+and allow your developers to concentrate solely on the business logic.
-Complete example:
+You can add a `stateObserver` to collect app metrics, an `errorObserver` to log errors,
+an `actionObserver` to print information to the console during development,
+and a `globalWrapError` to catch all errors.
```dart
-// This action increments a counter by 1, and then gets some description text.
-class IncrementAndGetDescriptionAction extends ReduxAction {
-
- @override
- Future reduce() async {
- dispatch(IncrementAction());
- String description = await read(Uri.http("numbersapi.com","${state.counter}");
- return state.copy(description: description);
- }
-
- void before() => dispatch(BarrierAction(true));
-
- void after() => dispatch(BarrierAction(false));
-}
+var store = Store(
+ stateObserver: [MyStateObserver()],
+ errorObserver: [MyErrorObserver()],
+ actionObservers: [MyActionObserver()],
+ globalWrapError: MyGlobalWrapError(),
```
-Try running
-the:
-Before and After Example.
+
-#### Wrapping the reducer
-
-You may wrap the reducer to allow for some pre or post-processing. For example, suppose you want to
-abort the reducer if the state changed since while the reducer was running:
+For example, the following `globalWrapError` handles `PlatformException` errors thrown
+by Firebase. It converts them into `UserException` errors, which are built-in types that
+automatically show a message to the user in an error dialog:
```dart
-Reducer wrapReduce(Reducer reduce) => () async {
- var oldState = state; // Remember: `state` is a getter for the current state.
- AppState? newState = await reduce(); // This may take some time, and meanwhile the state may change.
- return identical(oldState, state) ? newState : null;
-};
+Object? wrap(error, stackTrace, action) =>
+ (error is PlatformException)
+ ? UserException('Error connecting to Firebase')
+ : error;
+}
```
-#### Aborting the dispatch
-
-You may override the action's `abortDispatch` to completely prevent the action to run if some
-condition is true; In more detail, if this method returns `true`, methods `before`, `reduce`
-and `after` will not be called, and the action will not be visible to the `StoreTester`. This is
-only useful under rare circumstances, and you should only use it if you know what you are doing. For
-example:
-
-```dart
-@override
-bool abortDispatch() => state.user.name == null;
-```
+
-#### Action status
+# Advanced action configuration
-You can use `action.status.isCompletedOk` to check if a dispatched action finished with no
-errors (in more detail, if the action's methods `before` and `reduce` finished without throwing
-any errors):
+The Team Lead may create a base action class that all actions will extend, and add some common
+functionality to it. For example, getter shortcuts to important parts of the state,
+and selectors to help find information.
```dart
-var action = MyAction();
-await store.dispatchAndWait(action);
-print(action.isCompletedOk);
-```
-
-Better yet, you can get this information directly through the `dispatchAndWait` method:
-
-```dart
-var status = await store.dispatchAndWait(MyAction());
-print(status.isCompletedOk);
-```
+class AppState {
+ List items;
+ int selectedItem;
+}
-One use case is when you want to save some info, and you want to leave the current screen if and
-only if the save process succeeded:
+class Action extends ReduxAction {
-```dart
-class SaveAction extends ReduxAction {
- Future reduce() async {
- bool isSaved = await saveMyInfo();
- if (!isSaved) throw UserException("Save failed.");
- ...
- }
+ // Getter shortcuts
+ List get items => state.items;
+ Item get selectedItem => state.selectedItem;
+
+ // Selectors
+ Item? findById(int id) => items.firstWhereOrNull((item) => item.id == id);
+ Item? searchByText(String text) => items.firstWhereOrNull((item) => item.text.contains(text));
+ int get selectedIndex => items.indexOf(selectedItem);
}
-
-var status = await dispatch(SaveAction(info));
-if (status.isCompletedOk) Navigator.pop(context); // Or: dispatch(NavigateAction.pop())
```
-This is all the information you can get from the action status:
+
-```dart
-var status = await dispatch(MyAction(info));
-print(status.isCompleted);
-print(status.isCompletedOk);
-print(status.isCompletedFailed);
-print(status.originalError);
-print(status.wrappedError);
-print(status.status.hasFinishedMethodBefore);
-print(status.status.hasFinishedMethodReduce);
-print(status.status.hasFinishedMethodAfter);
-```
-
-#### What's the order of execution of sync and async reducers?
-
-A reducer is only sync if both `reduce()` return `AppState` AND `before()` return `void`. If you
-any of them return a `Future`, then the reducer is async.
-
-When you dispatch **sync** reducers, they are executed synchronously, in the order they are called.
-For example, this code will wait until `MyAction1` is finished and only then run `MyAction2`,
-assuming both are sync reducers:
-
-```dart
-dispatch(MyAction1());
-dispatch(MyAction2());
-```
-
-Dispatching an async reducer without writing `await` is like starting any other async processes
-without writing `await`. The process will start immediately, but you are not waiting for it to
-finish. For example, this code will start both `MyAsyncAction1` and `MyAsyncAction2`, but is says
-nothing about how long they will take or which one finishes first:
-
-```dart
-dispatch(MyAsyncAction1());
-dispatch(MyAsyncAction2());
-```
-
-If you want to wait for some async action to finish, you must write `await dispatch(...)`
-instead of simply `dispatch(...)`. Then you can actually wait for it to finish. For example, this
-code will wait until `MyAsyncAction1` is finished, and only then run `MyAsyncAction2`:
-
-```dart
-await dispatch(MyAsyncAction1());
-await dispatch(MyAsyncAction2());
-```
-
-
-
-## Using BuildContext to access the store
-
-To access the store state inside of widgets, you can use the provided extension on `context`:
-
-```dart
-// Read state (will rebuild when the state changes)
-var myInfo = context.state.myInfo;
-
-// Dispatch action
-context.dispatch(MyAction());
-
-// Use isWaiting to show a spinner
-var isWaiting = context.isWaiting(MyAction);
-
-// Use isFailed to show an error message
-if (context.isFailed(MyAction)) return Text('Loading failed');
-
-// Use exceptionFor to get the error message from the exception
-if (context.isFailed(MyAction)) return Text(context.exceptionFor(MyAction).message);
-
-// Use clearExceptionFor to clear the error
-context.clearExceptionFor(MyAction);
-```
-
-In more detail:
-
-* `var state = context.getState` - Reads the store state. Widgets that use this will
- rebuild whenever the state changes.
-
- It's recommended that you define this extension in your own code:
-
- ```dart
- extension BuildContextExtension on BuildContext {
- AppState get state => getState();
- }
- ```
-
- This will allow you to write:
- ```dart
- var state = context.state;
- ```
-
-
-* `context.dispatch()`, `.dispatchAndWait()` and `.dispatchSync()` - Dispatch an action.
-
-* `context.dispatchAll()`, and `.dispatchAndWaitAll()` - Dispatch multiple actions.
-
-* `context.isWaiting()` - Returns true if the given action type is currently being processed.
-
-* `context.isFailed()` - Returns true if an action of the given type failed with an `UserException`.
-
-* `context.exceptionFor()` - Returns the `UserException` of the action of the given type that
- failed.
-
-* `context.clearExceptionFor()` - Removes the given type from the list of action types that failed.
-
-Try running
-the:
-Connector vs Provider Example.
-
-## Connector
-
-In Redux, you generally have two widgets, one called the "dumb-widget", which knows nothing about
-Redux and the store, and another one to "wire" the store with that dumb-widget.
-
-While Vanilla Redux traditionally calls these wiring widgets "containers", Flutter's most common
-widget is already called a `Container`, which can be confusing. So I prefer calling them "
-connectors".
-
-They do their magic by using a `StoreConnector` and a `ViewModel`.
-
-A view-model is a helper object to a `StoreConnector` widget. It holds the part of the Store state
-the corresponding dumb-widget needs, and may also convert this state part into a more convenient
-format for the dumb-widget to work with.
-
-In more detail: Each time some action reducer changes the store state, all `StoreConnector`s in the
-screen will use that state to create a new view-model, and then compare it with the previous
-view-model created with the previous state. Only if the view-model changed, the connector rebuilds.
-
-For example:
-
-```dart
-class MyHomePageConnector extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return StoreConnector(
- vm: () => Factory(this),
- builder: (BuildContext context, ViewModel vm) => MyHomePage(
- counter: vm.counter,
- description: vm.description,
- onIncrement: vm.onIncrement,
- ));
- }}
-
-class Factory extends VmFactory {
- Factory(connector) : super(connector);
- @override
- ViewModel fromStore() => ViewModel(
- counter: state.counter,
- description: state.description,
- onIncrement: () => dispatch(IncrementAndGetDescriptionAction()),
- );
-}
-
-class ViewModel extends Vm {
- final int counter;
- final String description;
- final VoidCallback onIncrement;
- ViewModel({
- required this.counter,
- required this.description,
- required this.onIncrement,
- }) : super(equals: [counter, description]);
-}
-```
-
-For the view-model comparison to work, your ViewModel class must implement equals/hashcode.
-Otherwise, the `StoreConnector` will think the view-model changes everytime, and thus will rebuild
-everytime. This won't create any visible problems to your app, but is inefficient and may be slow.
-
-The equals/hashcode can be done in three ways:
-
-* By typing `ALT`+`INSERT` in IntelliJ IDEA and choosing `==() and hashcode`. You can't forget to
- update this whenever new parameters are added to the model.
-
-* You can use the built_value package to ensure
- they are kept correct, without you having to update them manually.
-
-* Just add all the fields you want (which are not callbacks) to the `equals` parameter to
- the `ViewModel`'s `build` constructor. This will allow the ViewModel to automatically create its
- own `operator ==` and `hashcode` implicitly. For example:
-
-```dart
-ViewModel({
- required this.field1,
- required this.field2,
-}) : super(equals: [field1, field2]);
-```
-
-Note: Each state passed in the `equals` parameter will, by default, be compared by equality (`==`).
-However, you can provide your own comparison method, if you want. To that end, your state classes
-may implement the `VmEquals` interface. As a default, objects of type `VmEquals` are compared by
-their `VmEquals.vmEquals()` method, which by default is an identity comparison. You may then
-override this method to provide your custom comparisons.
-
-For example, here `description` will be compared by equality, while `myObj` will be compared by
-its `info` length:
-
-```dart
-class ViewModel extends Vm {
- final String description;
- final MyObj myObj;
-
- ViewModel({
- required this.description,
- required this.myObj,
- }) : super(equals: [description, myObj]);
-}
-
-...
-
-class MyObj extends VmEquals {
- String info;
- bool operator ==(Object other) => info.length == other.info.length;
- int get hashCode => 0;
-}
-```
-
-
-
-### How to provide the ViewModel to the StoreConnector
-
-The `StoreConnector` actually accepts two parameters for the `ViewModel`, of which **only one**
-should be provided in the `StoreConnector` constructor: `vm` or `converter`.
-
-1. the `vm` parameter
-
- Most examples in the [example tab](https://pub.dartlang.org/packages/async_redux#-example-tab-)
- use the `vm` parameter.
-
- The `vm` parameter expects a function that creates a `Factory` object that extends
- `VmFactory`. This class should implement a method `fromStore` that returns a `ViewModel`
- that extends `Vm`:
-
- ```dart
- @override
- Widget build(BuildContext context) {
- return StoreConnector(
- vm: () => Factory(),
- builder: (BuildContext context, ViewModel vm) => MyWidget(...),
- );
- }
- ```
-
- AsyncRedux will automatically inject `state`, `currentState()` and `dispatch()` into your model
- instance, so that boilerplate is reduced in your `fromStore` method. For example:
-
- ```dart
- class Factory extends VmFactory {
-
- @override
- ViewModel fromStore() => ViewModel(
- counter: state.counter,
- description: state.description,
- onIncrement: () => dispatch(IncrementAndGetDescriptionAction()),
- );
- }
- ```
-
- **Note:**
-
- * `state` getter: The state the store was holding when the factory and the view-model were
- created. This state is final inside the factory.
-
- * `currentState()` method: The current (most recent) store state. This will return the current
- state the store holds at the time the method is called.
-
-
-
- If you need it, you may pass the connector widget to the factory's constructor, like this:
-
- ```dart
- vm: () => Factory(this),
-
- ...
-
- class Factory extends VmFactory {
- Factory(connector) : super(connector);
-
- @override
- ViewModel fromStore() => ViewModel(
- name: state.names[widget.user],
- );
- }
- ```
-
- The `vm` parameter's architecture lets you create separate methods for helping construct your
- model, without having to pass the `store` around. For example:
-
- ```dart
- @override
- ViewModel fromStore() => ViewModel(
- name: _name(),
- onSave: _onSave,
- );
-
- String _name() => state.user.name;
-
- VoidCallback _onSave: () => dispatch(SaveUserAction()),
- ```
-
- You can reference the view-model inside the Factory methods, by using the `vm` getter.
- For example:
-
- ```dart
- @override
- ViewModel fromStore() => ViewModel(
- name: state.user.name,
- onSave: _onSaveName,
- );
-
- // Use `vm.name` here.
- VoidCallback _onSaveName: () => dispatch(SaveUserAction(vm.name)),
- ```
-
- Please note, you can only use the `vm` getter after the `fromStore()` method returns, which
- means you cannot use it inside the `fromStore()` method itself. If you do that,
- you'll get a `StoreException`.
-
-
-
- Another idea is to subclass `VmFactory` to:
-
- * Reduce boilerplate, and not having to pass the `AppState` type parameter whenever you
- create a Factory.
-
- * Provide additional features to your model. For example, you could add extra getters to help
- you access state.
-
- Example:
-
- ```dart
- abstract class BaseFactory
- extends VmFactory {
-
- BaseFactory([T? connector]) : super(connector);
-
- User get user => state.user;
- }
-
- class _Factory extends BaseFactory {
-
- @override
- ViewModel fromStore() => ViewModel(
- name: user.name, // Instead of `name: state.user.name`
- );
- }
- ```
-
-
-
-**What if you can't generate the view-model?**
-
-Note: Sometimes you don't have enough information to generate the view-model. For example, some
-information may still be loading, or the state is inconsistent for some reason. In that case,
-your Factory can return `null` instead of the `vm`, and the connector can return an alternative
-placeholder widget.
-
-To that end, declare the view-model as nullable (`ViewModel?`) in these 3 places:
-the `StoreConnector`, the `builder`, and the `fromStore` method. Then, check for `null` in
-the `builder`. For example:
-
- ```dart
- return StoreConnector( // 1. Use `ViewModel?` here!
- vm: () => Factory(this),
- builder: (BuildContext context, ViewModel? vm) { // 2. Use `ViewModel?` here!
- return (vm == null) // 3. Check for null view-model here.
- ? Text("The user is not logged in")
- : MyHomePage(user: vm.user)
-
- ...
-
- class Factory extends VmFactory {
- ViewModel? fromStore() { // 4. Use `ViewModel?` here!
- return (store.state.user == null)
- ? null
- : ViewModel(user: store.state.user)
-
- ...
-
- class ViewModel extends Vm {
- final User user;
- ViewModel({required this.user}) : super(equals: [user]);
- ```
-
-Try running
-the:
-Null ViewModel Example.
-
-
-
-2. The `converter` parameter
-
- If you are migrating from `flutter_redux` to `async_redux`, you can keep using `flutter_redux`'s
- good old `converter` parameter:
-
- ```dart
- @override
- Widget build(BuildContext context) {
- return StoreConnector(
- converter: (store) => ViewModel.fromStore(store),
- builder: (BuildContext context, ViewModel vm) => MyWidget(...),
- );
- }
- ```
-
- It expects a static factory function that gets a `store` and returns the `ViewModel`.
-
- ```dart
- class ViewModel {
- final String name;
- final VoidCallback onSave;
-
- ViewModel({
- required this.name,
- required this.onSave,
- });
-
- static ViewModel fromStore(Store store) {
- return ViewModel(
- name: store.state,
- onSave: () => store.dispatch(IncrementAction(amount: 1)),
- );
- }
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) ||
- other is ViewModel && runtimeType == other.runtimeType && name == other.name;
-
- @override
- int get hashCode => name.hashCode;
- }
- ```
-
-However, the `converter` parameter can also make use of the `Vm` class to avoid having to
-create `operator ==` and `hashcode` manually:
-
- ```dart
- class ViewModel extends Vm {
- final String name;
- final VoidCallback onSave;
-
- ViewModel({
- required this.name,
- required this.onSave,
- }) : super(equals: [name]);
-
- static ViewModel fromStore(Store store) {
- return ViewModel(
- name: store.state,
- onSave: () => store.dispatch(IncrementAction(amount: 1)),
- );
- }
- }
- ```
-
-When using the `converter` parameter, it's a bit more difficult to create separate methods for
-helping construct your view-model:
-
- ```dart
- static ViewModel fromStore(Store store) {
- return ViewModel(
- name: _name(store),
- onSave: _onSave(store),
- );
- }
-
- static String _name(Store) => store.state.user.name;
-
- static VoidCallback _onSave(Store) {
- return () => store.dispatch(SaveUserAction());
- }
- ```
-
-To see the `converter` parameter in action, please run
-
-this example.
-
-#### Will a state change always trigger the StoreConnectors?
-
-Usually yes, but if you want you can order some action not to trigger the `StoreConnector`, by
-providing a `notify: false` when dispatching:
-
-```dart
-dispatch(MyAction1(), notify: false);
-```
-
-
-
-### Provider
-
-Another good alternative to the `StoreConnector` is using
-the Provider
-package.
-
-Both the `StoreConnector` (from *async_redux*) and `ReduxSelector` (from *provider_for_redux*)
-let you deal with widget rebuilds when the state changes.
-
-You may use `StoreConnector` when you want to have two widgets, one to access the store and prepare
-the state to use, and the second as a dumb widget. You may use `ReduxSelector` when you want less
-boilerplate, and want to access the store directly from inside a single widget.
-
-Please visit the provider_for_redux
-package for in-depth explanation and examples on how to use AsyncRedux and Provider together.
-
-
-
-## Processing errors thrown by Actions
-
-AsyncRedux has special provisions for dealing with errors, including observing errors, showing
-errors to users, and wrapping errors into more meaningful descriptions.
-
-Let's see an example. Suppose a logout action that checks if there is an internet connection, and
-then deletes the database and sets the store to its initial state:
-
-```dart
-class LogoutAction extends ReduxAction {
- @override
- Future reduce() async {
- await checkInternetConnection();
- await deleteDatabase();
- dispatch(NavigateToLoginScreenAction());
- return AppState.initialState();
- }
-}
-```
-
-In the above code, the `checkInternetConnection()` function checks if there is an
-internet connection, and if there isn't it
-throws an error:
-
-```dart
-Future checkInternetConnection() async {
- if (await Connectivity().checkConnectivity() == ConnectivityResult.none)
- throw NoInternetConnectionException();
-}
-```
-
-All errors thrown by action reducers are sent to the **ErrorObserver**, which you may define during
-store creation. For example:
-
-```dart
-var store = Store(
- initialState: AppState.initialState(),
- errorObserver: MyErrorObserver(),
-);
-
-class MyErrorObserver implements ErrorObserver {
- @override
- bool observe(Object error, StackTrace stackTrace, ReduxAction action, Store store) {
- print("Error thrown during $action: $error");
- return true;
- }
-}
-```
-
-If your error observer returns `true`, the error will be rethrown after the `errorObserver`
-finishes. If it returns `false`, the error is considered dealt with, and will be "swallowed" (not
-rethrown).
-
-
-
-### Giving better error messages
-
-If your reducer throws some error you probably want to collect as much information as possible. In
-the above code, if `checkInternetConnection()` throws an error, you want to know that you have a
-connection problem, but you also want to know this happened during the logout action. In fact, you
-want all errors thrown by this action to reflect that.
-
-The solution is implementing the optional `wrapError(error)` method:
-
-```dart
-class LogoutAction extends ReduxAction {
-
- @override
- Future reduce() async { ... }
-
- @override
- Object wrapError(error)
- => LogoutError("Logout failed.", cause: error);
-}
-```
-
-Note the `LogoutError` above gets the original error as cause, so no information is lost.
-
-In other words, the `wrapError(error)` method acts as the "catch" statement of the action.
-
-
-
-### User exceptions
-
-To show error messages to the user, make your actions throw an `UserException`, and then wrap your
-home-page with `UserExceptionDialog`, below `StoreProvider` and `MaterialApp`:
-
-```dart
-class MyApp extends StatelessWidget {
- @override
- Widget build(BuildContext context)
- => StoreProvider(
- store: store,
- child: MaterialApp(
- navigatorKey: navigatorKey,
- home: UserExceptionDialog(
- child: MyHomePage(),
- )));
-}
-```
-
-Note: If you are not using the `home` parameter of the `MaterialApp` widget, you can also put the
-`UserExceptionDialog` the `builder` parameter. Please note, if you do that you **must** define the
-`NavigateAction.navigatorKey` of the Navigator. Please, see the documentation of the
-`UserExceptionDialog.useLocalContext` parameter for more information.
-
-Try running
-the:
-Show Error Dialog Example.
-
-**In more detail:**
-
-Sometimes, actions fail because the user provided invalid information. These failings don't
-represent errors in the code, so you usually don't want to log them as errors. What you want,
-instead, is just warn the user by opening a dialog with some corrective information. For example,
-suppose you want to save the user's name, and you only accept names with at least 4 characters:
-
-```dart
-class SaveUserAction extends ReduxAction {
- final String name;
- SaveUserAction(this.name);
-
- @override
- Future reduce() async {
- if (name.length < 4) dispatch(ShowDialogAction("Name must have at least 4 letters."));
- else await saveUser(name);
- return null;
- }
-}
-```
-
-Clearly, there is no need to log as an error the user's attempt to save a 3-char name. The above
-code dispatches a `ShowDialogAction`, which you would have to wire into a Flutter error dialog
-somehow.
-
-However, there's an easier approach. Just throw AsyncRedux's built-in `UserException`:
-
-```dart
-class SaveUserAction extends ReduxAction {
- final String name;
- SaveUserAction(this.name);
-
- @override
- Future reduce() async {
- if (name.length < 4) throw UserException("Name must have at least 4 letters.");
- await saveName(name);
- return null;
- }
-}
-```
-
-The special `UserException` error class represents "user errors" which are meant as warnings to the
-user, and not as code errors to be logged. By default, if you don't define your own `errorObserver`,
-only errors which are not `UserException` are thrown. And if you do define an `errorObserver`,
-you'd probably want to replicate this behavior.
-
-In any case, `UserException`s are put into a special error queue, from where they may be shown to
-the user, one by one. You may use `UserException` as is, or subclass it, returning title and message
-for the alert dialog shown to the user. _Note: In the `Store` constructor you can set the maximum
-number of errors that queue can hold._
-
-As explained in the beginning of this section, if you use the build-in error handling you must wrap
-your home-page with `UserExceptionDialog`. There, you may pass the `onShowUserExceptionDialog`
-parameter to change the default dialog, show a toast, or some other suitable widget:
-
-```dart
-UserExceptionDialog(
- child: MyHomePage(),
- onShowUserExceptionDialog:
- (BuildContext context, UserException userException) => showDialog(...),
-);
-```
-
-> Note: The `UserExceptionDialog` can display any error widget you want in front of all the others
-> on the screen. If this is not what you want, you can easily create your
-> own `MyUserExceptionWidget` to intercept the errors and do whatever you want. Start by
-> copying `user_exception_dialog.dart` (which contains `UserExceptionDialog` and its `_ViewModel`)
-> into another file, and search for the `didUpdateWidget` method. This method will be called each
-> time an error is available, and there you can record this information in the widget's own state.
-> You can then change the screen in any way you want, according to that saved state, in this
-> widget's `build` method.
-
-
-
-### Converting third-party errors into UserExceptions
-
-Third-party code may also throw errors which should not be considered bugs, but simply messages to
-be displayed in a dialog to the user.
-
-For example, Firebase my throw some `PlatformException`s in response to a bad connection to the
-server. In this case, you can convert this error into a `UserException`, so that a dialog appears to
-the user, as already explained above. There are two ways to do that.
-
-The first is to do this conversion in the action itself by implementing the
-optional `ReduxAction.wrapError(error)` method:
-
-```dart
-class MyAction extends ReduxAction {
-
- @override
- Object? wrapError(error) {
- if ((error is PlatformException) && (error.code == "Error performing get") &&
- (error.message == "Failed to get document because the client is offline."))
- return UserException("Check your internet connection.").addCause(error);
- else
- return error;
- }
-```
-
-However, then you'd have to add this to all actions that use Firebase. A better way is doing this
-globally by passing a `GlobalWrapError` object to the store:
-
-```dart
-var store = Store(
- initialState: AppState.initialState(),
- globalWrapError: MyGlobalWrapError(),
-);
-
-class MyGlobalWrapError extends GlobalWrapError {
- @override
- Object? wrap(error, stackTrace, action) {
- if ((error is PlatformException) && (error.code == "Error performing get") &&
- (error.message == "Failed to get document because the client is offline."))
- return UserException("Check your internet connection.").addCause(error);
- else
- return error;
- }
-}
-```
-
-The `GlobalWrapError` object will be given all errors. It may then return a `UserException` which
-will be used instead of the original exception. Otherwise, it just returns the original `eerror`,
-so that it will not be modified. It may also return `null` to disable (swallow) the error.
-
-Note this wrapper is called **after** `ReduxAction.wrapError`, and **before** the `ErrorObserver`.
-
-
-
-### UserExceptionAction
-
-If you want the `UserExceptionDialog` to display some `UserException`, you must throw the exception
-from inside an action's `before()` or `reduce()` methods.
-
-However, sometimes you need to create some **callback** that throws an `UserException`. If this
-callback is called **outside** an action, the dialog will **not** display the exception. To solve
-this, the callback should not throw an exception, but instead call the
-provided `UserExceptionAction`, which will then simply throw the exception in its own `reduce()`
-method.
-
-The `UserExceptionAction` is also useful even inside of actions, when you want to display an error
-dialog to the user, but you don't want to interrupt the action by throwing an exception.
-
-
-
-## Testing
-
-Testing involves waiting for an action to complete its dispatch process,
-or for the store state to meet a certain condition. After this, you can verify the current
-state or action using the
-methods `store.dispatchAndWait`, `store.dispatchAndWaitAll`, `store.waitCondition`,
-`store.waitActionCondition`, `store.waitAllActions`, `store.waitActionType`,
-`store.waitAllActionTypes`, and `store.waitAnyActionTypeFinishes`. For example:
-
-```dart
-// Wait for some action to dispatch and check the state.
-await store.dispatchAndWait(MyAction());
-expect(store.state.name, 'John')
-
-// Wait for some action to dispatch, and check for errors in the action status.
-var status = await dispatchAndWait(MyAction());
-expect(status.originalError, isA());
-
-// Dispatches two actions in SERIES (one after the other).
-await dispatchAndWait(SomeAsyncAction());
-await dispatchAndWait(AnotherAsyncAction());
-
-// Dispatches two actions in PARALLEL and wait for their TYPES.
-expect(store.state.portfolio, ['TSLA']);
-dispatch(BuyAction('IBM'));
-dispatch(SellAction('TSLA'));
-await store.waitAllActionTypes([BuyAction, SellAction]);
-expect(store.state.portfolio, ['IBM']);
-
-// Dispatches two actions in PARALLEL and wait for them.
-let action1 = BuyAction('IBM');
-let action2 = BuyAction('TSLA');
-dispatch(action1);
-dispatch(action2);
-await store.waitAllActions([action1, action2]);
-expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);
-
-// Another way to dispatch two actions in PARALLEL and wait for them.
-await store.dispatchAndWaitAll([BuyAction('IBM'), BuyAction('TSLA')]);
-expect(store.state.portfolio.containsAll('IBM', 'TSLA'), isFalse);
-
-// Wait until no actions are in progress.
-dispatch(BuyStock('IBM'));
-dispatch(BuyStock('TSLA'));
-await waitAllActions([]);
-expect(state.stocks, ['IBM', 'TSLA']);
-
-// Wait for some action of a given type.
-dispatch(ChangeNameAction());
-var action = store.waitActionType(ChangeNameAction);
-expect(action, isA());
-expect(action.status.isCompleteOk, isTrue);
-expect(store.state.name, 'Bill');
-
-// Wait until any action of the given types finishes dispatching.
-dispatch(BuyOrSellAction());
-var action = store.waitAnyActionTypeFinishes([BuyAction, SellAction]);
-expect(store.state.portfolio.contains('IBM'), isTrue);
-
-// Wait for some state condition.
-expect(store.state.name, 'John')
-dispatch(ChangeNameAction("Bill"));
-var action = await store.waitCondition((state) => state.name == "Bill");
-expect(action, isA());
-expect(store.state.name, 'Bill');
-```
-
-## Testing with StoreTester (deprecated)
-
-For **almost all tests** it's now recommended to use the `Store` directly, as shown in the previous
-section.
-
-Older/deprecated code may still use the old `StoreTester`.
-Start by creating the store-tester from a store:
-
-```dart
-var store = Store(initialState: AppState.initialState());
-var storeTester = StoreTester.from(store);
-```
-
-Or else, creating it directly from `AppState`:
-
-```dart
-var storeTester = StoreTester(initialState: AppState.initialState());
-```
-
-Then, dispatch some action, wait for it to finish, and check the resulting state:
-
-```dart
-storeTester.dispatch(SaveNameAction("Mark"));
-TestInfo info = await storeTester.wait(SaveNameAction);
-expect(info.state.name, "Mark");
-```
-
-or
-
-```dart
-TestInfo info = storeTester.dispatchAndWait(SaveNameAction("Mark"));
-expect(info.state.name, "Mark");
-```
-
-The variable `info` above will contain information about after the action reducer finishes
-executing, **no matter if the reducer is sync or async**.
-
-The `TestInfo` instance contains the following:
-
-* `state`: The store state.
-* `action`: The dispatched Action that resulted in that state.
-* `ini`: A boolean which indicates true if this info represents the "initial" state right before the
- action is dispatched, or false it represents the "end" state right after the action finishes
- executing.
-* `dispatchCount`: The number of dispatched actions so far.
-* `reduceCount`: The number of reduced states so far.
-* `errors`: The `UserException`s the store was holding when the information was gathered.
-
-While the above example demonstrates the testing of a simple action, real-world apps have actions
-that dispatch other actions. You may use different `StoreTester` methods to check if the expected
-actions are dispatched, and test their intermediary states.
-
-Let's see all the available methods of the `StoreTester`:
-
-1. `Future wait(Type actionType)`
-
- Expects **one action** of the given type to be dispatched, and waits until it finishes. Returns
- the info after the action finishes. Will fail with an exception if an unexpected action is seen.
-
-2. `Future waitUntil(Type actionType)`
-
- Runs until an action of the given type is dispatched, and then waits until it finishes. Returns
- the info after the action finishes.
- **Ignores other** actions types.
-
-3. `Future waitUntilAll(List actionTypes)`
-
- Runs until all actions of the given types are dispatched and finish, in any order. Returns a list
- with all info until the last action finishes. **Ignores other** actions types.
-
-4. `Future waitUntilAllGetLast(List actionTypes)`
-
- Runs until all actions of the given types are dispatched and finish, in any order. Returns the
- info after they all finish. **Ignores other** actions types.
-
-5. `Future waitUntilAction(ReduxAction action)`
-
- Runs until the exact given action is dispatched, and then waits until it finishes. Returns the
- info after the action finishes. **Ignores other** actions.
-
-6. `Future dispatchAndWait(ReduxAction action)`
-
- Dispatches the given action, then waits until it finishes. Returns the
- info after the action finishes. **Ignores other** actions.
-
-7. `Future waitAllGetLast(List actionTypes, {List ignore})`
-
- Runs until **all** given actions types are dispatched, **in order**. Waits until all of them are
- finished. Returns the info after all actions finish. Will fail with an exception if an unexpected
- action is seen, or if any of the expected actions are dispatched in the wrong order. To ignore
- some actions, pass them to the `ignore` list.
-
-8. `Future waitAllUnorderedGetLast(List actionTypes, {List ignore})`
-
- Runs until **all** given actions types are dispatched, in **any order**. Waits until all of them
- are finished. Returns the info after all actions finish. Will fail with an exception if an
- unexpected action is seen. To ignore some actions, pass them to the `ignore` list.
-
-9. `Future waitAll(List actionTypes, {List ignore})`
-
- The same as `waitAllGetLast`, but instead of returning just the last info, it returns a list with
- the end info for each action. To ignore some actions, pass them to the `ignore` list.
-
-10. `Future waitAllUnordered(List actionTypes, {List ignore})`
-
- The same as `waitAllUnorderedGetLast`, but instead of returning just the last info, it returns a
- list with the end info for each action. To ignore some actions, pass them to the `ignore` list.
-
-11. `Future> waitCondition(StateCondition condition, {bool testImmediately = true, bool ignoreIni = true})`
-
- Runs until the predicate function `condition` returns true. This function will receive each
- testInfo, from where it can access the state, action, errors etc. When `testImmediately` is
- true (the default), it will test the condition immediately when the method is called. If the
- condition is true, the method will return immediately, without waiting for any actions to be
- dispatched. When `testImmediately` is false, it will only test the condition once an action is
- dispatched. Only END states will be received, unless you pass `ignoreIni` as false. Returns a
- list with all info until the condition is met.
-
-12. `Future> waitConditionGetLast(StateCondition condition, {bool testImmediately = true, bool ignoreIni = true})`
-
- Runs until the predicate function `condition` returns true. This function will receive each
- testInfo, from where it can access the state, action, errors etc. When `testImmediately` is
- true (the default), it will test the condition immediately when the method is called. If the
- condition is true, the method will return immediately, without waiting for any actions to be
- dispatched. When `testImmediately` is false, it will only test the condition once an action is
- dispatched. Only END states will be received, unless you pass `ignoreIni` as false. Returns the
- info after the condition is met.
-
-13. `Future> waitUntilError({Object error, Object processedError})`
-
- Runs until after an action throws an error of this exact type, or this exact error (using
- equals). You can also, instead, define `processedError`, which is the error after wrapped by the
- action's `wrapError()` method. Returns a list with all info until the error condition is met.
-
-14. `Future waitUntilErrorGetLast({Object error, Object processedError})`
-
- Runs until after an action throws an error of this exact type, or this exact error (using
- equals). You can also, instead, define `processedError`, which is the error after wrapped by the
- action's `wrapError()` method. Returns the info after the condition is met.
-
-15. `Future> dispatchState(St state)`
-
- Dispatches an action that changes the current state to the one provided by you. Then, runs until
- that action is dispatched and finished (ignoring other actions). Returns the info after the
- action finishes, containing the given state.
-
-Some of the methods above return a list of type `TestInfoList`, which contains the step by step
-information of all the actions. You can then query for the actions you want to inspect. For example,
-suppose an action named `IncrementAndGetDescriptionAction` calls another 3 actions. You can assert
-that all actions are called in order, and then get the state after each one of them have finished,
-all at once:
-
-```dart
-var storeTester = StoreTester(initialState: AppState.initialState());
-expect(storeTester.state.counter, 0);
-expect(storeTester.state.description, isEmpty);
-
-storeTester.dispatch(IncrementAndGetDescriptionAction());
-
-TestInfoList infos = await storeTester.waitAll([
- IncrementAndGetDescriptionAction,
- BarrierAction,
- IncrementAction,
- BarrierAction,
-]);
-
-// Modal barrier is turned on (first time BarrierAction is dispatched).
-expect(infos.get(BarrierAction, 1).state.waiting, true);
-
-// While the counter was incremented the barrier was on.
-expect(infos[IncrementAction].waiting, true);
-
-// Then the modal barrier is dismissed (second time BarrierAction is dispatched).
-expect(infos.get(BarrierAction, 2).state.waiting, false);
-
-// In the end, counter is incremented, description is created, and barrier is dismissed.
-var info = infos[IncrementAndGetDescriptionAction];
-expect(info.state.waiting, false);
-expect(info.state.description, isNotEmpty);
-expect(info.state.counter, 1);
-```
-
-Try running
-the:
-Testing with the Store Listener.
-
-Also,
-the
-tests of the StoreTester can also serve as examples.
-
-**Important:** The `StoreTester` has access to the current store state via `StoreTester.state`, but
-you should not try to assert directly from this state. This would seem to work most of the time, but
-by the time you do the assert, the state could already have been changed by some other action. To
-avoid that, always assert from the `info` you get from the `StoreTester` methods, which is
-guaranteed to be the one right after your *wait condition* is achieved. For example:
-
-```dart
-// This is right:
-TestInfo info = await storeTester.wait(SaveNameAction);
-expect(info.state.name, "Mark");
-
-// This is wrong:
-await storeTester.wait(SaveNameAction);
-expect(storeTester.state.name, "Mark");
-```
-
-However, to help you further reduce your test boilerplate, the last `info`
-obtained from the most recent wait condition is saved into a variable called `storeTester.lastInfo`:
-
-```dart
-// This:
-TestInfo info = await storeTester.wait(SaveNameAction);
-expect(info.state.name, "Mark");
-
-// Is the same as this:
-await storeTester.wait(SaveNameAction);
-expect(storeTester.lastInfo.state.name, "Mark");
-```
-
-
-
-### Testing the StoreConnector's View-model
-
-To test the view-model generated by a `VmFactory`, use `createFrom` and pass it the
-`store` and the `factory`. Note this method must be called in a recently
-created factory, as it can only be called once per factory instance.
-
-The method will return the **view-model**, which you can use to:
-
-* Inspect the view-model properties directly, or
-
-* Call any of the view-model callbacks. If the callbacks dispatch actions,
- you use `await store.waitAllActions([])`,
- or `await store.waitActionType(MyAction)`,
- or `await store.waitAllActionTypes([MyAction, OtherAction])`,
- or `await store.waitAnyActionTypeFinishes([MyAction, OtherAction])`,
- or `await store.waitCondition((state) => ...)`,
- or if necessary you can even record all dispatched actions and state changes
- with `Store.record.start()` and `Store.record.stop()`.
-
-Example:
-
-```
-var store = Store(initialState: User("Mary"));
-var vm = Vm.createFrom(store, MyFactory());
-
-// Checking a view-model property.
-expect(vm.user.name, "Mary");
-
-// Calling a view-model callback and waiting for the action to finish.
-vm.onChangeNameTo("Bill"); // Dispatches SetNameAction("Bill").
-await store.waitActionType(SetNameAction);
-expect(store.state.name, "Bill");
-
-// Calling a view-model callback and waiting for the state to change.
-vm.onChangeNameTo("Bill"); // Dispatches SetNameAction("Bill").
-await store.waitCondition((state) => state.name == "Bill");
-expect(store.state.name, "Bill");
-```
-
-#### Testing the StoreConnector's onInit and onDispose
-
-Suppose you want to start polling information when your user enters a particular screen, and stop
-when the user leaves it. This could be your `StoreConnector`:
-
-```dart
-class MyScreen extends StatelessWidget {
- @override
- Widget build(BuildContext context) => StoreConnector(
- vm: () => _Factory(),
- onInit: _onInit,
- onDispose: _onDispose,
- builder: (context, vm) => MyWidget(...),
- );
-
- void _onInit(Store store) => store.dispatch(PollInformationAction(true));
- void _onDispose(Store store) => store.dispatch(PollInformationAction(false));
-}
-```
-
-You want to test that `onInit` and `onDispose` above dispatch the correct action to start and stop
-polling. You may achieve this by creating a widget test, entering and leaving the screen, and then
-using the `StoreTester` to check that the actions were dispatched.
-
-Instead, you may simply use the `ConnectorTester`, which you can access from the `StoreTester`:
-
-```dart
-var storeTester = StoreTester(...);
-var connectorTester = storeTester.getConnectorTester(MyScreen());
-
-connectorTester.runOnInit();
-var info = await storeTester.waitUntil(PollInformationAction);
-expect((info.action as PollInformationAction).start, true);
-
-connectorTester.runOnDispose();
-info = await storeTester.waitUntil(PollInformationAction);
-expect((info.action as PollInformationAction).start, false);
-```
-
-
-
-### Mocking actions and reducers
-
-To mock an action and its reducer, create a `MockStore` instead of a regular `Store`.
-
-The `MockStore` has a `mocks` parameter which is a map where the keys are action types, and the
-values are the mocks. For example:
-
-```dart
-var store = MockStore(
- initialState: initialState,
- mocks: {
- MyAction1 : ...
- MyAction2 : ...
- ...
- },
-);
-```
-
-There are 5 different ways to define mocks:
-
-1. Use `null` to disable dispatching the action of a certain type:
-
- ```dart
- mocks: {
- MyAction : null
- }
- ```
-
-2. Use a `MockAction` instance to dispatch this mock action instead, and provide the **original
- action** as a getter to the mock action.
-
- ```dart
- class MyAction extends ReduxAction {
- String url;
- MyAction(this.url);
- Future reduce() => get(url);
- }
-
- class MyMockAction extends MockAction {
- Future reduce() async {
- String url = (action as MyAction).url;
- if (url == 'https://example.com') return 123;
- else if (url == 'https://flutter.io') return 345;
- else return 678;
- }
- }
- ```
-
- ```dart
- mocks: {
- MyAction : MyMockAction()
- }
- ```
-
-3. Use a `ReduxAction` instance to dispatch this mock action instead.
-
- ```dart
- class MyAction extends ReduxAction {
- String url;
- MyAction(this.url);
- Future reduce() => get(url);
- }
-
- class MyMockAction extends ReduxAction {
- Future reduce() async => 123;
- }
- ```
-
- ```dart
- mocks: {
- MyAction : MyMockAction()
- }
- ```
-
-4. Use a `ReduxAction Function(ReduxAction)` to create a mock from the original action.
-
- ```dart
- class MyAction extends ReduxAction {
- String url;
- MyAction(this.url);
- Future reduce() => get(url);
- }
-
- class MyMockAction extends MockAction {
- String url;
- MyMockAction(this.url);
- Future reduce() async {
- if (url == 'https://example.com') return 123;
- else if (url == 'https://flutter.io') return 345;
- else return 678;
- }
- }
- ```
-
- ```dart
- mocks: {
- MyAction : (MyAction action) => MyMockAction(action.url)
- }
- ```
-
-5. Use a `St Function(ReduxAction, St)`
- or `Future Function(ReduxAction, St)`
- to modify the state directly.
-
- ```dart
- class MyAction extends ReduxAction {
- String url;
- MyAction(this.url);
- Future reduce() => get(url);
- }
- ```
-
- ```dart
- mocks: {
- MyAction : (MyAction action, AppState state) async {
- if (action.url == 'https://example.com') return 123;
- else if (action.url == 'https://flutter.io') return 345;
- else return 678;
- }
- }
- ```
-
-You can also change the mocks after a store is created, by using the following methods of
-the `MockStore` and `StoreTester` classes:
-
-```dart
-MockStore addMock(Type actionType, dynamic mock);
-MockStore addMocks(Map mocks);
-MockStore clearMocks();
-```
-
-
-
-### Testing UserExceptions
-
-Since `UserException`s don't represent bugs in the code, AsyncRedux put them into the
-store's `errors` queue, and then swallows them. This is usually what you want during production,
-where errors from this queue are shown in a dialog to the user. But it may or may not be what you
-want during tests.
-
-In tests there are two possibilities:
-
-1. You are testing that some `UserException` is thrown. For example, you want to test that users are
- warned if they typed letters in some field that only accepts numbers. To that end, your test
- would dispatch the appropriate action, and then check if the `errors` queue now contains
- an `UserException` with some specific error message.
-
-2. You are testing some code that should **not** throw any exceptions. If the test has thrown an
- exception it means the test has failed, and the exception should show up in the console, for
- debugging. However, this won't work if when test throws an `UserException` it simply go to
- the `errors` queue. If this happens, the test will continue running, and may even pass. The only
- way to make sure no errors were thrown would be asserting that the `errors` queue is still empty
- at the end of the test. This is even more problematic if the unexpected `UserException` is thrown
- inside a `before()` method. In this case it will prevent the reducer to run, and the test will
- probably fail with wrong state but no errors in the console.
-
-The solution is to use the `shouldThrowUserExceptions` parameter in the `StoreTester` constructor.
-
-Pass `shouldThrowUserExceptions` as `true`, and all errors will be thrown and not swallowed,
-including `UserException`s. Use this in all tests that should throw no errors:
-
-```dart
-var storeTester = StoreTester(
- initialState: AppState.initialState(),
- shouldThrowUserExceptions: true);
-```
-
-Pass `shouldThrowUserExceptions` as false (the default)
-when you are testing code that should indeed throw `UserExceptions`. These exceptions will then
-silently go to the `errors` queue, where you can assert they exist and have the right error
-messages:
-
-```dart
-storeTester.dispatch(MyAction());
-TestInfo info = await storeTester.waitAllGetLast([MyAction]);
-expect(info.errors.removeFirst().msg, "You can't do this.");
-```
-
-
-
-### Test files
-
-If you want your tests to be comprehensive you should probably have 3 different types of test for
-each widget:
-
-1. **State Tests** — Test the state of the app, including actions/reducers. This type of tests make
- use of the `StoreTester` described above.
-
-2. **Connector Tests** — Test the connection between the store and the "dumb-widget". In other words
- it tests the "connector-widget" and the "view-model".
-
-3. **Presentation Tests** — Test the UI. In other words it tests the "dumb-widget", making sure that
- the widget displays correctly depending on the parameters you use in its constructor. You pass in
- the data the widget requires in each test for rendering, and then writes assertions against the
- rendered output. Think of these tests as "pure function tests" of our UI. It also tests that the
- callbacks are called when necessary.
-
-For example, suppose you have the counter app
-shown
-here. Then:
-
-* The **state test** could create a store with count `0` and description empty, and then
- dispatch `IncrementAction` and expect the count to become `1`. Then it could test
- dispatching `IncrementAndGetDescriptionAction` alters the count to `2`
- and the description to some non-empty string.
-
-* The **connector test** would create a store and a page with the `MyHomePageConnector` widget. It
- would then access the `MyHomePage` and make sure it gets the expected info from the store, and
- also that the expected `IncrementAndGetDescriptionAction` is dispatched when the "+" button is
- tapped.
-
-* The **presentation test** would create the `MyHomePage` widget, pass `counter:0`
- and `description:"abc"` parameters in its constructor, and make sure they appear in the screen as
- expected. It would also test that the callback is called when the "+" button is tapped.
-
-Since each widget will have a bunch of related files, you should have some consistent naming
-convention. For example, if some dumb-widget is called `MyWidget`, its file could
-be `my_widget.dart`. Then the corresponding connector-widget could be `MyWidgetConnector`
-in `my_widget_CONNECTOR.dart`. The three corresponding test files could be
-named `my_widget_STATE_test.dart`, `my_widget_CONNECTOR_test.dart`
-and `my_widget_PRESENTATION_test.dart`. If you don't like this convention use your own,
-but just choose one early and stick to it.
-
-
-
-## Route Navigation
-
-AsyncRedux comes with a `NavigateAction` which you can dispatch to navigate your Flutter app. For
-this to work, during app initialization you must create a navigator key and then inject it into the
-action:
-
-```dart
-final navigatorKey = GlobalKey();
-
-void main() async {
- NavigateAction.setNavigatorKey(navigatorKey);
- ...
-}
-```
-
-You must also use this same navigator key in your `MaterialApp`:
-
-```dart
-return StoreProvider(
- store: store,
- child: MaterialApp(
- ...
- navigatorKey: navigatorKey,
- initialRoute: '/',
- onGenerateRoute: ...
- ),
-);
-```
-
-Then, use the action as needed:
-
-```dart
-// Most Navigator methods are available.
-// For example pushNamed:
-dispatch(NavigateAction.pushNamed("myRoute"));
-```
-
-Note: Don't ever save the current route in the store. This will create all sorts of problems. If you
-need to know the route you're in, you may use this static method provided by `NavigateAction`:
-
-```dart
-String routeName = NavigateAction.getCurrentNavigatorRouteName(context);
-```
-
-Try running
-the:
-Navigate Example.
-
-### Testing with `NavigateAction`
-
-You can test navigation by asserting navigation types, route names etc. This is useful for verifying
-app flow in unit tests, instead of widget or driver tests.
-
-For example:
-
-```dart
-var navigateAction = actions.get(NavigateAction).action as NavigateAction;
-expect(navigateAction.type, NavigateType.pushNamed);
-expect((navigateAction.details as NavigatorDetails_PushNamed).routeName, "myRoute");
-```
-
-
-
-## Events
-
-In a real Flutter app it's not practical to assume that a Redux store can hold all the
-application state. Widgets like `TextField` and `ListView` make use of controllers, which hold
-state, and the store must be able to work alongside these. For example, in response to the
-dispatching of some action you may want to clear the text-field, or you may want to scroll the
-list-view to the top. Even when no controllers are involved, you may want to execute some one-off
-processes, like opening a dialog or closing the keyboard, and it's not obvious how to do that in
-vanilla Redux.
-
-AsyncRedux solves these problems by introducing the concept of "events". The naming convention is
-that Events are named with the `Evt` suffix.
-
-Boolean events can be created like this:
-
-```dart
-var clearTextEvt = Event();
-```
-
-But you can have events with payloads of any other data type. For example:
-
-```dart
-var changeTextEvt = Event("Hello");
-var myEvt = Event(42);
-```
-
-Events may be put into the store state in their "spent" state, by calling its `spent()` constructor.
-For example, while creating the store initial-state:
-
-```dart
-static AppState initialState() {
- return AppState(
- clearTextEvt: Event.spent(),
- changeTextEvt: Event.spent(),
-}
-```
-
-And then events may be passed down by the `StoreConnector` to some `StatefulWidget`, just like any
-other state:
-
-```dart
-class MyConnector extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return StoreConnector(
- model: ViewModel(),
- builder: (BuildContext context, ViewModel vm) => MyWidget(
- initialText: vm.initialText,
- clearTextEvt: vm.clearTextEvt,
- changeTextEvt: vm.changeTextEvt,
- onClear: vm.onClear,
- ));
- }
-}
-
-class ViewModel extends BaseModel {
- ViewModel();
-
- String initialText;
- Event clearTextEvt;
- Event changeTextEvt;
-
- ViewModel.build({
- required this.initialText,
- required this.clearTextEvt,
- required this.changeTextEvt,
- }) : super(equals: [initialText, clearTextEvt, changeTextEvt]);
-
- @override
- ViewModel fromStore() => ViewModel.build(
- initialText: state.initialText,
- clearTextEvt: state.clearTextEvt,
- changeTextEvt: state.changeTextEvt,
- onClear: () => dispatch(ClearTextAction()),
- );
-}
-
-class ClearTextAction extends ReduxAction {
- @override
- AppState reduce() => state.copy(changeTextEvt: Event());
-}
-
-class ChangeTextAction extends ReduxAction {
- String newText;
- ChangeTextAction(this.newText);
-
- @override
- AppState reduce() => state.copy(changeTextEvt: Event(newText));
-}
-```
-
-This is how it differs: The dumb-widget will "consume" the event in its `didUpdateWidget`
-method, and do something with the event payload:
-
-```dart
-@override
-void didUpdateWidget(MyWidget oldWidget) {
- super.didUpdateWidget(oldWidget);
- consumeEvents();
-}
-
-void consumeEvents() {
- if (widget.clearTextEvt.consume()) { // Do something }
-
- var payload = widget.changeTextEvt.consume();
- if (payload != null) { // Do something }
-}
-```
-
-The `evt.consume()` will return the payload once, and then that event is considered "spent".
-
-In more detail, if the event **has no value and no generic type**, then it's a boolean event. This
-means `evt.consume()` returns **true** once, and then **false** for subsequent calls. However, if
-the event **has value or some generic type**, then `Event.consume()` returns the **value** once, and
-then **null** for subsequent calls.
-
-So, for example, if you use a `controller` to hold the text in a `TextField`:
-
-```dart
-void consumeEvents() {
-
- if (widget.clearTextEvt.consume())
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (mounted) controller.clear();
- });
-
- String newText = widget.changeTextEvt.consume();
- if (newText != null)
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (mounted) controller.value = controller.value.copyWith(text: newText);
- });
- }
-```
-
-Try running
-the:
-Event Example.
-
-
-
-### Can I put mutable events into the store state?
-
-Events are mutable, and store state is supposed to be immutable. Won't this create problems? No!
-Don't worry, events are used in a contained way, and were crafted to play well with the Redux
-infrastructure. In special, their `equals()` and `hashcode()` methods make sure no unnecessary
-widget rebuilds happen when they are used as prescribed.
-
-You can think of events as piggybacking in the Redux infrastructure, and not belonging to the store
-state. You should just remember **not to persist them** when you persist the store state.
-
-
-
-### When should I use events?
-
-The short answer is that you'll know it when you see it. When you want to do something, and it's not
-obvious how to do it by changing regular store state, it's probably easy to solve it if you try
-using events instead.
-
-However, we can also give these guidelines:
-
-1. You may use regular store state to pass constructor parameters to both stateless and stateful
- widgets.
-2. You may use events to change the internal state of stateful widgets, after they are built.
-3. You may use events to make one-off changes in controllers.
-4. You may use events to make one-off changes in other implicit state like the open state of dialogs
- or the keyboard.
-
-
-
-### Advanced event features
-
-There are some advanced event features you may not need, but you should know they exist:
-
-1. Methods `isSpent`, `isNotSpent` and `state`
-
- Methods `isSpent` and `isNotSpent` tell you if an event is spent or not, without consuming the
- event. Method `state` returns the event payload, without consuming the event.
-
-2. Constructor `Event.map(Event evt, T Function(dynamic) mapFunction)`
-
- This is a convenience factory to create an event which is transformed by some function that,
- usually, needs the store state. You must provide the event and a map-function. The map-function
- must be able to deal with the spent state (`null` or `false`, accordingly).
-
- For example, if `state.indexEvt = Event(5)` and you must get a user from it:
-
- ```dart
- var mapFunction = (index) => index == null ? null : state.users[index];
- Event userEvt = MappedEvent(state.indexEvt, mapFunction);
- ```
-
-3. Constructor `Event.from(Event evt1, Event evt2)`
-
- This is a convenience factory method to create `EventMultiple`, a special type of event which
- consumes from more than one event. If the first event is not spent, it will be consumed, and the
- second will not. If the first event is spent, the second one will be consumed. So, if both events
- are NOT spent, the method will have to be called twice to consume both. If both are spent,
- returns `null`.
-
-4. Method `static T consumeFrom(Event evt1, Event evt2)`
-
- This is a convenience static method to consume from more than one event. If the first event is
- not spent, it will be consumed, and the second will not. If the first event is spent, the second
- one will be consumed. So, if both events are NOT spent, the method will have to be called twice
- to consume both. If both are spent, returns `null`. For example:
-
- ```dart
- String getMessageEvt() => Event.consumeFrom(firstMsgEvt, secondMsgEvt);
- ```
-
-
-
-## Progress indicators
-
-A **progress indicator** is a visual indication that some important process is taking some time to
-finish (and will hopefully finish soon). For example:
-
-* A save button that displays a `CircularProgressIndicator` while some info is saving.
-
-* A `Text("Please wait...")` that is displayed in the center of the screen while some info is being
- calculated.
-
-* A shimmer that is displayed as a placeholder
- while some widget info is being downloaded.
-
-* A modal barrier that prevents the user from interacting with the screen while some info is loading
- or saving.
-
-
-
-The easiest way to show a progress indicator is to use `store.isWaiting(MyAction)`,
-where `MyAction` is the async action you are waiting for. This works well for the majority of cases.
-
-Try running
-the:
-Show Spinner Example. When you press the "+" button, it dispatches an increment action that
-takes 2 seconds to finish. Meanwhile, a spinner is shown in the button, and the counter text gets
-grey.
-
-In [Before and After the Reducer](#before-and-after-the-reducer) section I show how to manually
-create a boolean flag that is used to add or remove a modal barrier in the screen (see the
-code
-here). This will work in some rare complex cases where `store.isWaiting()` is not enough.
-
-However, keeping track of many such boolean flags may be difficult to do.
-If you need help with this problem, an option is using the built-in classes `WaitAction` and `Wait`.
-
-For this to work, your store state must have a `Wait` field named `wait`, and then your state class
-must have a `copy` or a `copyWith` method which copies this field as a named parameter. For example:
-
-```dart
-@immutable
-class AppState {
- final Wait wait;
- ...
- AppState({this.wait, ...});
- AppState copy({Wait wait, ...}) => AppState(wait: wait ?? this.wait, ...);
- }
-```
-
-Then, when you want to start waiting, simply dispatch a `WaitAction`
-and pass it some immutable object to act as a flag. When you finish waiting, just remove the flag.
-For example:
-
-```dart
-dispatch(WaitAction.add("my flag")); // To add a flag.
-dispatch(WaitAction.remove("my flag")); // To remove a flag.
-```
-
-When you are using the state:
-
-* `state.wait.isWaitingAny` returns `true` if there's any waiting whatsoever.
-* `state.wait.isWaiting(flag)` returns `true` if you are waiting for a specific `flag`
-* `state.wait.isWaiting(flag, ref: reference)` returns `true` if you are waiting for a
- specific `reference` of the `flag`.
-* `state.wait.isWaitingForType()` returns `true` if you are waiting for any flag of type `T`.
-
-
-
-The flag can be any convenient **immutable object**, like a URL, a user id, an index, an enum, a
-String, a number, or other.
-
-As an example, if we want to replace the `store.isWaiting()` method with the `Wait` object, we
-could do this: Suppose that a button dispatches a `LoadAction` to load some text. You can make the
-button show a progress indicator while the text is being loaded, and show the text when it's done:
-
-```dart
-class LoadAction extends ReduxAction {
-
- Future reduce() async {
- var newText = await loadText();
- return state.copy(text: newText);
- }
-
- void before() => dispatch(WaitAction.add(this));
- void after() => dispatch(WaitAction.remove(this));
-}
-```
-
-Then, in the button:
-
-```dart
-if (wait.isWaitingForType()) { // Show the button as disabled }
-else { // Show the button as enabled }
-```
-
-Note: You may also define a **mixin** to implement the waiting:
-
-```dart
-mixin WithWaitState implements ReduxAction {
- void before() => dispatch(WaitAction.add(this));
- void after() => dispatch(WaitAction.remove(this));
-}
-
-class LoadAction extends ReduxAction with WithWaitState {
- Future reduce() async {
- var newText = await loadText();
- return state.copy(text: newText);
- }
-}
-```
-
-Try running
-the:
-Wait Action Simple Example
-(which is similar to the
-
-Before and After example but using the built-in `WaitAction`). It uses the action itself as the
-flag, by passing `this`.
-
-
-
-A more advanced example is
-the
-Wait Action Advanced 1 Example. Here, 10 buttons are shown. When a button is clicked it will be
-replaced by a downloaded text description. Each button shows a progress indicator while its
-description is downloading. Also, the screen title shows the text `"Downloading..."` if any of the
-buttons is currently downloading.
-
-![](https://github.com/marcglasberg/async_redux/blob/master/example/lib/images/waitAction.png)
-
-The flag in this case is simply the index of the button, from `0` to `9`:
-
-```dart
-int index;
-void before() => dispatch(WaitAction.add(index));
-void after() => dispatch(WaitAction.remove(index));
-```
-
-In the `ViewModel`, just as before, if there's any waiting, then `state.wait.isWaitingAny` will
-return `true`. However, now you can check each button wait flag separately by its index.
-`state.wait.isWaiting(index)` will return `true` if that specific button is waiting.
-
-Note: If necessary, you can clear all flags by doing `dispatch(WaitAction.clear())`.
-
-
-
-If you fear your flag may conflict with others, you can also add a "namespace", by further dividing
-flags into references. This can be seen in
-the
-Wait Action Advanced 2 Example:
-
-```dart
-void before() => dispatch(WaitAction.add("button-download", ref: index));
-void after() => dispatch(WaitAction.remove("button-download", ref: index));
-```
-
-Now, to check a button's wait flag, you must pass both the flag and the reference:
-`state.wait.isWaiting("button-download", ref: index)`.
-
-Note: If necessary, you can clear all references of that flag by
-doing `dispatch(WaitAction.clear("button-download"))`.
-
-You can also pass a delay to `WaitAction.add()` and `WaitAction.remove()` methods. Please refer to
-their method documentation for more information.
-
-### Using BuiltValue, Freezed, or other similar code generator packages
-
-In case you use
-built_value
-or freezed packages, the `WaitAction` works
-out-of-the-box with them. In both cases, you don't need to create the `copy` or `copyWith` methods
-by hand. But you still need to add the `Wait` object to the store state as previously described.
-
-If you want to further customize `WaitAction` to work with other packages, or to use the `Wait`
-object in different ways, you can do so by injecting your custom reducer into `WaitAction.reducer`
-during your app's initialization. Refer to
-the `WaitAction`
-documentation for more information.
-
-
-
-## Showing a RefreshIndicator while some action is running
-
-In a real Flutter app it's also the case that some Widgets ask for futures that complete when some
-async process is done.
-
-The `dispatchAndWait()` function returns a future which completes as soon as the action is done.
-
-This is an example using the `RefreshIndicator` widget:
-
-```dart
-Future downloadStuff() => dispatchAndWait(DownloadStuffAction());
-
-return RefreshIndicator(
- onRefresh: downloadStuff;
- child: ListView(...),
-```
-
-Or, if you have multiple actions you can use `dispatchAndWaitAll`.
-
-Try running
-the:
-Dispatch Future Example.
-
-
-
-## Waiting until the state meets a certain condition
-
-The `waitCondition` method from the `Store` class lets you create **futures** that complete when the
-store state meets a certain condition:
-
-```dart
-/// Returns a future which will complete when the given condition is true.
-/// The condition can access the state. You may also provide a
-/// timeoutInSeconds, which by default is null (never times out).
-Future waitCondition(
- bool Function(St) condition, {
- int timeoutInSeconds
- })
-```
-
-For example:
-
-```dart
-class SaveAppointmentAction extends ReduxAction {
- final Appointment appointment;
-
- SaveAppointmentAction(this.appointment);
-
- @override
- Future reduce() {
- dispatch(CreateCalendarIfNecessaryAction());
- await store.waitCondition((state) => state.calendar != null);
- return state.copy(calendar: state.calendar.copyAdding(appointment));
- }
-}
-```
-
-The above action needs to add an appointment to a calendar, but it can only do that if the calendar
-already exists. Maybe the calendar already exists, but if not, creating a calendar is a complex
-async process, which may take some time to complete.
-
-To that end, the action dispatches an action to create the calendar if necessary, and then use
-the `store.waitCondition()` method to wait until a calendar is present in the state. Only then it
-will add the appointment to the calendar.
-
-
-
-## State Declaration
-
-While your main state class, usually called `AppState`, may be simple and contain all the state
-directly, in a real world application you will probably want to create many state classes and add
-them to the main state class. For example, if you have some state for the login, some user related
-state, and some *todos* in a To-Do app, you can organize it like this:
-
-```dart
-@immutable
-class AppState {
-
- final LoginState loginState;
- final UserState userState;
- final TodoState todoState;
-
- AppState({
- this.loginState,
- this.userState,
- this.todoState,
- });
-
- AppState copy({
- LoginState loginState,
- UserState userState,
- TodoState todoState,
- }) {
- return AppState(
- login: loginState ?? this.loginState,
- user: userState ?? this.userState,
- todo: todoState ?? this.todoState,
- );
- }
-
- static AppState initialState() =>
- AppState(
- loginState: LoginState.initialState(),
- userState: UserState.initialState(),
- todoState: TodoState.initialState());
-
- @override
- bool operator ==(Object other) =>
- identical(this, other) || other is AppState && runtimeType == other.runtimeType &&
- loginState == other.loginState && userState == other.userState && todoState == other.todoState;
-
- @override
- int get hashCode => loginState.hashCode ^ userState.hashCode ^ todoState.hashCode;
-}
-```
-
-All of your state classes may follow this pattern. For example, the `TodoState` could be like this:
-
-```dart
-import 'package:flutter/foundation.dart';
-import 'package:collection/collection.dart';
-
-class TodoState {
-
- final List todos;
-
- TodoState({this.todos});
-
- TodoState copy({List todos}) {
- return TodoState(
- todos: todos ?? this.todos);
- }
-
- static TodoState initialState() => TodoState(todos: const []);
-
- @override
- bool operator ==(Object other) {
- return identical(this, other) || other is TodoState && runtimeType == other.runtimeType &&
- listEquals(todos, other.todos);
- }
-
- @override
- int get hashCode => const ListEquality.hash(todos);
-}
-```
-
-
-
-### Selectors
-
-Your connector-widgets usually have a view-model that goes into the store and selects the part of
-the store the widget needs. If you have some "selecting logic" that you use in different places, you
-may create a "selector". Selectors may be put in separate files, or they may be put into state
-classes, as static methods. For example, the `TodoState` class above could contain a selector to
-filter out some todos:
-
-```dart
-static List selectTodosForUser(AppState state, User user)
- => state.todoState.todos.where((todo) => (todo.user == user)).toList();
-```
-
-
-
-### Cache (Reselectors)
-
-Suppose you use a `ListView.builder` to display user names as list items. In your `StoreConnector`,
-you could create a `ViewModel` that, given the item index, returns a user name:
-
-```dart
-state.users[index].name;
-```
-
-But now suppose you want to display only the users with names that start with the letter `A`. You
-could filter the user list to remove all other names, like this:
-
-```dart
-state.users.where((user)=>user.name.startsWith("A")).toList()[index].name;
-```
-
-This works, but will filter the list repeatedly, once for each index. This is not a problem for
-small lists, but will become slow if the list contains thousands of users.
-
-The solution to this problem is caching the filtered list. To that end, you can use the "reselect"
-functionality provided by AsyncRedux.
-
-First, create a selector that returns the information you need:
-
-```dart
-static List selectUsersWithNamesStartingWith(AppState state, {String text})
- => state.users.where((user)=>user.name.startsWith(text)).toList();
-```
-
-And then use it in the ViewModel:
-
-```dart
-selectUsersWithNamesStartingWith(state, text: "A")[index].name;
-```
-
-Next, we have to modify the selector so that it caches the filtered list. AsyncRedux provides a few
-global functions which you can use, depending on the number of states, and the number of parameters
-your selector needs.
-
-In this example, we have a single state and a single parameter, so we're going to use the `cache1_1`
-method:
-
-```dart
-static List selectUsersWithNamesStartingWith(AppState state, {String text})
- => _selectUsersWithNamesStartingWith(state)(text);
-
-static final _selectUsersWithNamesStartingWith = cache1_1(
- (AppState state)
- => (String text)
- => state.users.where((user)=>user.name.startsWith(text)).toList());
-```
-
-The above code will calculate the filtered list only once, and then return it when the selector is
-called again with the same `state` and `text` parameters.
-
-If the `state` changes, or the `text` changes (or both), it will recalculate and then cache again
-the new result.
-
-We can further improve this by noting that we only need to recalculate the result when `state.users`
-changes. Since `state.users` is a subset of `state`, it will change less often. So a better selector
-would be this:
-
-```dart
-static List selectUsersWithNamesStartingWith(AppState state, {String text})
- => _selectUsersWithNamesStartingWith(state.users)(text);
-
-static final _selectUsersWithNamesStartingWith = cache1_1(
- (List users)
- => (String text)
- => users.where((user)=>user.name.startsWith(text)).toList());
-```
-
-#### Cache syntax
-
-For the moment, AsyncRedux provides these six methods that combine 1 or 2 states with 0, 1 or 2
-parameters:
-
-```dart
-cache1((state) => () => ...);
-cache1_1((state) => (parameter) => ...);
-cache1_2((state) => (parameter1, parameter2) => ...);
-
-cache2((state1, state2) => () => ...);
-cache2_1((state1, state2) => (parameter) => ...);
-cache2_2((state1, state2) => (parameter1, parameter2) => ...);
-```
-
-I have created only those above, because for my own usage I never required more than that. Please,
-open an issue
-to ask for more variations in case you feel the need.
-
-This syntax treats the states and the parameters differently. If you call some selector while
-keeping the **same state** and changing only the parameter, the selector will cache all the results,
-one for each parameter.
-
-However, as soon as you call the selector with a **changed state**, it will delete all of its
-previous cached information, since it understands that they are no longer useful. And even if you
-don't call that selector ever again, it will delete the cached information if it detects that the
-state is no longer used in other parts of the program. In other words, AsyncRedux keeps the cached
-information in weak-map, so that the cache will not
-hold to old information and have a negative impact in memory usage.
-
-#### The reselect package
-
-The reselect functionality explained above is provided out-of-the-box with AsyncRedux. However,
-AsyncRedux also works perfectly with the external
-reselect package.
-
-Then, why did I care to reimplement a similar functionality? What are the differences?
-
-First, the AsyncRedux caches can keep any number of cached results for each selector, one for each
-time the selector is called with the same states and different parameters. Meanwhile, the reselect
-package keeps a single cached result per selector.
-
-And second, the AsyncRedux reselector discards the cached information when the state changes or is
-no longer used. Meanwhile, the reselect package will always keep the states and cached results in
-memory.
-
-
-
-## Action Subclassing
-
-Suppose you have the following `AddTodoAction` for the To-Do app:
-
-```dart
-class AddTodoAction extends ReduxAction {
- final Todo todo;
- AddTodoAction(this.todo);
-
- @override
- AppState reduce() {
- if (todo == null) return null;
- else return state.copy(todoState: List.of(state.todoState.todos)..add(todo));
- }
-}
-
-// You would use it like this:
-store.dispatch(AddTodoAction(Todo("Buy some beer.")));
-```
-
-Since all actions extend `ReduxAction`, you may further use object oriented principles to reduce
-boilerplate. Start by creating an **abstract** action base class to allow easier access to the
-sub-states of your store. For example:
-
-```dart
-abstract class BaseAction extends ReduxAction {
- LoginState get loginState => state.loginState;
- UserState get userState => state.userState;
- TodoState get todoState => state.todoState;
- List get todos => todoState.todos;
-}
-```
-
-And then your actions have an easier time accessing the store state:
-
-```dart
-class AddTodoAction extends BaseAction {
- final Todo todo;
- AddTodoAction(this.todo);
-
- @override
- AppState reduce() {
- if (todo == null) return null;
- else return state.copy(todoState: List.of(todos)..add(todo)));
- }
-}
-```
-
-As you can see above, instead of writing `List.of(state.todoState.todos)` you can simply
-write `List.of(todos)`. It may seem a small reduction of boilerplate, but it adds up.
-
-
-
-### Abstract Before and After
-
-Other useful abstract classes you may create provide already overridden `before()` and `after()`
-methods. For example, this abstract class turns on a modal barrier when the action starts, and
-removes it when the action finishes:
-
-```dart
-abstract class BarrierAction extends ReduxAction {
- void before() => dispatch(BarrierAction(true));
- void after() => dispatch(BarrierAction(false));
-}
-```
-
-Then you could use it like this:
-
-```dart
-class ChangeTextAction extends BarrierAction {
-
- @override
- Future reduce() async {
- String newText = await read(Uri.http("numbersapi.com","${state.counter}");
- return state.copy(
- counter: state.counter + 1,
- changeTextEvt: Event(newText));
- }
-}
-```
-
-The above `BarrierAction` is demonstrated
-in
-this example.
-
-
-
-## Dependency Injection
-
-While you can always use get_it or any other
-dependency injection solution, AsyncRedux lets you inject your dependencies directly in the
-**store**, and then access them in your actions and view-model factories.
-
-The dependency injection idea was contributed by Craig
-McMahon.
-
-To inject an environment object with the dependencies:
-
-```dart
-store = Store(
- initialState: ...,
- environment: Environment(),
-);
-```
-
-You can then extend both `ReduxAction` and `VmFactory` to provide typed access to your environment:
-
-```dart
-abstract class AppFactory extends VmFactory {
- AppFactory([T? connector]) : super(connector);
-
- @override
- Environment get env => super.env as Environment;
-}
-
-
-abstract class Action extends ReduxAction {
-
- @override
- Environment get env => super.env as Environment;
-}
-```
-
-Then, use the environment when creating the view-model:
-
-```dart
-class Factory extends AppFactory {
- Factory(connector) : super(connector);
-
- @override
- ViewModel fromStore() => ViewModel(
- counter: env.limit(state),
- onIncrement: () => dispatch(IncrementAction(amount: 1)),
- );
-}
-
-```
-
-And also in your actions:
-
-```dart
-class IncrementAction extends Action {
- final int amount;
- IncrementAction({required this.amount});
-
- @override
- int reduce() => env.incrementer(state, amount);
-}
-```
-
-Try running
-the:
-Dependency Injection Example.
-
-## IDE Navigation
-
-How does AsyncRedux solve the IDE navigation problem?
-
-During development, if you need to see what some action does, you just tell your IDE to navigate to
-the action itself
-(`CTRL+B` in IntelliJ/Windows, for example) and you have the reducer right there.
-
-If you need to list all of your actions, you just go to the `ReduxAction` class declaration and ask
-the IDE to list all of its subclasses.
-
-
-
-## Persistence
-
-Your store optionally accepts a `persistor`, which may be used for local persistence, i.e., keeping
-the current app state saved to the local disk of the device.
-
-You should create your own `MyPersistor` class which extends the `Persistor` abstract class. This is
-the recommended way to use it:
-
-```dart
-var persistor = MyPersistor();
-
-var initialState = await persistor.readState();
-
-if (initialState == null) {
- initialState = AppState.initialState();
- await persistor.saveInitialState(initialState);
- }
-
-var store = Store(
- initialState: initialState,
- persistor: persistor,
-);
-```
-
-As you can see above, when the app starts you use the `readState` method to try and read the state
-from the disk. If this method returns `null`, you must create an initial state and save it. You then
-create the store with the `initialState` and the `persistor`.
-
-This is the `Persistor` implementation:
-
-```dart
-abstract class Persistor {
- Future readState();
- Future deleteState();
- Future persistDifference({required St? lastPersistedState, required St newState});
- Future saveInitialState(St state) => persistDifference(lastPersistedState: null, newState: state);
- Duration get throttle => const Duration(seconds: 2);
-}
-```
-
-The `persistDifference` method is the one you should implement to be notified whenever you must save
-the state. It gets the `newState` and the `lastPersistedState`, so that you can compare them and
-save the difference. Or, if your app state is simple, you can simply save the whole `newState` each
-time the method is called.
-
-The `persistDifference` method will be called by AsyncRedux whenever the state changes, but not more
-than once each 2 seconds, which is the throttle period. All state changes within these 2 seconds
-will be collected, and then a single call to the method will be made with all the changes after this
-period.
-
-Also, the `persistDifference` method won't be called while the previous save is not finished, even
-if the throttle period is done. In this case, if a new state becomes available the method will be
-called as soon as the current save finishes.
-
-Note you can also override the `throttle` getter to define a different throttle period. In special,
-if you define it as `null` there will be no throttle, and you'll be able to save the state as soon
-as it changes.
-
-Even if you have a non-zero throttle period, sometimes you may want to save the state immediately.
-This is usually the case, for example, when the app is closing. You can do that by dispatching the
-provided `PersistAction`. This action will ignore the throttle period and call
-the `persistDifference` method right away to save the current state.
-
-```dart
-store.dispatch(PersistAction());
-```
-
-You can use this code to help you start extending the abstract `Persistor` class:
-
-```dart
-class MyPersistor extends Persistor {
-
- @override
- Future readState() async {
- // TODO: Put here the code to read the state from disk.
- return null;
- }
-
- @override
- Future deleteState() async {
- // TODO: Put here the code to delete the state from disk.
- }
-
- @override
- Future persistDifference({
- required AppState? lastPersistedState,
- required AppState newState,
- }) async {
- // TODO: Put here the code to save the state to disk.
- }
-
- @override
- Future saveInitialState(AppState state) =>
- persistDifference(lastPersistedState: null, newState: state);
-
- @override
- Duration get throttle => const Duration(seconds: 2);
-}
-
-```
-
-The persistor can also be paused and resumed, with methods `store.pausePersistor()`,
-`store.persistAndPausePersistor()` and `store.resumePersistor()`. This may be used together with the
-app lifecycle, to prevent a persistence process to start when the app is being shut down.
-
-When logging out of the app, you can call `store.deletePersistedState()` to ask the persistor to
-delete the state from disk.
-
-First, add an `AppLifecycleManager` to your widget tree:
-
-```dart
-...
-child: StoreProvider(
- store: store,
- child: AppLifecycleManager(
- child: MaterialApp( ...
-```
-
-Then, create the `AppLifecycleManager` with a `WidgetsBindingObserver` that dispatches
-a `ProcessLifecycleChange_Action`:
-
-```dart
-class AppLifecycleManager extends StatefulWidget {
- final Widget child;
- const AppLifecycleManager({Key? key, required this.child}) : super(key: key);
- _AppLifecycleManagerState createState() => _AppLifecycleManagerState();
-}
-
-class _AppLifecycleManagerState extends State with WidgetsBindingObserver {
-
- void initState() {
- super.initState();
- WidgetsBinding.instance.addObserver(this);
- }
-
- void dispose() {
- WidgetsBinding.instance.removeObserver(this);
- super.dispose();
- }
-
- void didChangeAppLifecycleState(AppLifecycleState lifecycle) {
- store.dispatch(ProcessLifecycleChange_Action(lifecycle));
- }
-
- Widget build(BuildContext context) => widget.child;
-}
-```
-
-Finally, define your `ProcessLifecycleChange_Action` to pause and resume the persistor:
-
-```dart
-class ProcessLifecycleChangeAction extends ReduxAction {
- final AppLifecycleState lifecycle;
- ProcessLifecycleChangeAction(this.lifecycle);
-
- @override
- Future reduce() async {
- if (lifecycle == AppLifecycleState.resumed || lifecycle == AppLifecycleState.inactive) {
- store.resumePersistor();
- } else if (lifecycle == AppLifecycleState.paused || lifecycle == AppLifecycleState.detached) {
- store.persistAndPausePersistor();
- } else
- throw AssertionError(lifecycle);
-
- return null;
- }
-}
-```
-
-Have a look at
-the:
-Persistence tests.
-
-
-
-### Saving and Loading
-
-You can choose any way you want to save the state difference to the local disk, but one way is using
-the provided `LocalPersist` class, which is very easy to use.
-
-Note: At the moment it only works for Android/iOS, not for the web.
-
-First, import it:
-
-```dart
-import 'package:async_redux/local_persist.dart';
-```
-
-You need to convert yourself your objects to a list of **simple objects**
-composed only of numbers, booleans, strings, lists and maps (you can nest lists and maps).
-
-For example, this is a list of simple objects:
-
-```dart
-List