async_redux

Last updated:

0 purchases

async_redux Image
async_redux Images
Add to Cart

Description:

async redux

Async Redux | state management #

Simple to learn and easy to use
Powerful enough to handle complex applications with millions of users
Testable

This means you'll be able to create apps much faster,
and other people on your team will easily understand and modify your code.
What is it? #
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
(see all my packages).

There is also a version for React


Optionally use it with Provider
or Flutter Hooks

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:
// Here, the state is a number
var store = Store<int>(initialState: 1);
copied to clipboard
// Here, the state is an object
class AppState {
final String name;
final int age;
State(this.name, this.age);
}

var store = Store<AppState>(initialState: AppState('Mary', 25));
copied to clipboard

To use the store, add it in a StoreProvider at the top of your widget tree.
Widget build(context) {
return StoreProvider<int>(
store: store,
child: MaterialApp( ... ),
);
}
copied to clipboard

Widgets use the state #
class MyWidget extends StatelessWidget {

Widget build(context) {
return Text('${context.state.name} has ${context.state.age} years old');
}
}
copied to clipboard

Actions and reducers #
An action is a class that contain its own reducer.
class Increment extends Action {

// The reducer has access to the current state
int reduce() => state + 1; // It returns a new state
}
copied to clipboard

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.
// 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()]);
copied to clipboard

Widgets can dispatch actions #
The context extensions to dispatch actions are dispatch , dispatchAll etc.
class MyWidget extends StatelessWidget {

Widget build(context) {
return ElevatedButton(
onPressed: () => context.dispatch(Increment());
}
}
copied to clipboard

Actions can do asynchronous work #
They download information from the internet, or do any other async work.
var store = Store<String>(initialState: '');
copied to clipboard
class LoadText extends Action {

// This reducer returns a Future
Future<String> reduce() async {

// Download something from the internet
var response = await http.get('https://dummyjson.com/todos/1');

// Change the state with the downloaded information
return response.body;
}
}
copied to clipboard


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.
class LoadText extends Action {

Future<String> reduce() async {
var response = await http.get('https://dummyjson.com/todos/1');

if (response.statusCode == 200) return response.body;
else throw UserException('Failed to load');
}
}
copied to clipboard

To show a spinner while an asynchronous action is running, use isWaiting(action).
To show an error message inside the widget, use isFailed(action).
class MyWidget extends StatelessWidget {

Widget build(context) {

if (context.isWaiting(LoadText)) return CircularProgressIndicator();
if (context.isFailed(LoadText)) return Text('Loading failed...');
return Text(context.state);
}
}
copied to clipboard

Actions can dispatch other actions #
You can use dispatchAndWait to dispatch an action and wait for it to finish.
class LoadTextAndIncrement extends Action {

Future<AppState> reduce() async {

// Dispatch and wait for the action to finish
await dispatchAndWait(LoadText());

// Only then, increment the state
return state.copy(count: state.count + 1);
}
}
copied to clipboard

You can also dispatch actions in parallel and wait for them to finish:
class BuyAndSell extends Action {

Future<AppState> reduce() async {

// Dispatch and wait for both actions to finish
await dispatchAndWaitAll([
BuyAction('IBM'),
SellAction('TSLA')
]);

return state.copy(message: 'New cash balance is ${state.cash}');
}
}
copied to clipboard

You can also use waitCondition to wait until the state changes in a certain way:
class SellStockForPrice extends Action {
final String stock;
final double limitPrice;
SellStockForPrice(this.stock, this.limitPrice);

Future<AppState?> reduce() async {

// Wait until the stock price is higher than the limit price
await waitCondition(
(state) => state.stocks[stock].price >= limitPrice
);

// Only then, post the sell order to the backend
var amount = await postSellOrder(stock);

return state.copy(
stocks: state.stocks.setAmount(stock, amount),
);
}
copied to clipboard

Add features to your actions #
You can add mixins to your actions, to accomplish common tasks.
Check for Internet connectivity #
CheckInternet ensures actions only run with internet,
otherwise an error dialog prompts users to check their connection:
class LoadText extends Action with CheckInternet {

Future<String> reduce() async {
var response = await http.get('https://dummyjson.com/todos/1');
...
}
}
copied to clipboard

NoDialog can be added to CheckInternet so that no dialog is opened.
Instead, you can display some information in your widgets:
class LoadText extends Action with CheckInternet, NoDialog {
...
}

class MyWidget extends StatelessWidget {
Widget build(context) {
if (context.isFailed(LoadText)) Text('No Internet connection');
}
}
copied to clipboard

AbortWhenNoInternet aborts the action silently (without showing any dialogs) if there is no
internet connection.

NonReentrant #
To prevent an action from being dispatched while it's already running,
add the NonReentrant mixin to your action class.
class LoadText extends Action with NonReentrant {
...
}
copied to clipboard

Retry #
Add Retry to retry the action a few times with exponential backoff, if it fails.
Add UnlimitedRetries to retry indefinitely:
class LoadText extends Action with Retry, UnlimitedRetries {
...
}
copied to clipboard

UnlimitedRetryCheckInternet #
Add UnlimitedRetryCheckInternet to check if there is internet when you run some action that needs
it. If there is no internet, the action will abort silently and then retried unlimited times,
until there is internet. It will also retry if there is internet but the action failed.
class LoadText extends Action with UnlimitedRetryCheckInternet {
...
}
copied to clipboard
Debounce (soon) #
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.
class SearchText extends Action with Debounce {
final String searchTerm;
SearchText(this.searchTerm);

final int debounce = 350; // Milliseconds

Future<AppState> reduce() async {

var response = await http.get(
Uri.parse('https://example.com/?q=' + encoded(searchTerm))
);

return state.copy(searchResult: response.body);
}
}
copied to clipboard

Throttle (soon) #
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.
class LoadPrices extends Action with Throttle {

final int throttle = 5000; // Milliseconds

Future<AppState> reduce() async {
var result = await loadJson('https://example.com/prices');
return state.copy(prices: result);
}
}
copied to clipboard

OptimisticUpdate (soon) #
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.
class SaveName extends Action with OptimisticUpdate {

async reduce() { ... }
}
copied to clipboard

Events #
Flutter widgets like TextField and ListView hold their own internal state.
You can use Events to interact with them.
// 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));
}
}

// Action that scrolls a ListView to the top
class ScrollToTop extends Action {
AppState reduce() => state.copy(scroll: Event(0));
}
}
copied to clipboard

Persist the state #
You can add a persistor to save the state to the local device disk.
var store = Store<AppState>(
persistor: MyPersistor(),
);
copied to clipboard

Testing your app is easy #
Just dispatch actions and wait for them to finish.
Then, verify the new state or check if some error was thrown.
class AppState {
List<String> items;
int selectedItem;
}

test('Selecting an item', () async {

var store = Store<AppState>(
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));
});
copied to clipboard

Advanced setup #
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.
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.
var store = Store<String>(
stateObserver: [MyStateObserver()],
errorObserver: [MyErrorObserver()],
actionObservers: [MyActionObserver()],
globalWrapError: MyGlobalWrapError(),
copied to clipboard

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:
Object? wrap(error, stackTrace, action) =>
(error is PlatformException)
? UserException('Error connecting to Firebase')
: error;
}
copied to clipboard

Advanced action configuration #
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.
class AppState {
List<Item> items;
int selectedItem;
}

class Action extends ReduxAction<AppState> {

// Getter shortcuts
List<Item> 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);
}
copied to clipboard

Now, all actions can use them to access the state in their reducers:
class SelectItem extends Action {
final int id;
SelectItem(this.id);

AppState reduce() {
Item? item = findById(id);
if (item == null) throw UserException('Item not found');
return state.copy(selected: item);
}
}
copied to clipboard

To learn more, the complete Async Redux documentation is published at https://asyncredux.com

The AsyncRedux code is based upon packages redux by
Brian Egan, and flutter_redux by Brian Egan and
John Ryan. Also uses code from package equatable by
Felix Angelov. The dependency injection idea in AsyncRedux was contributed by Craig McMahon. Special
thanks: Eduardo Yamauchi and Hugo Passos helped me with the async code, checking the documentation,
testing everything and making suggestions. This work started after Thomas Burkhart explained to me
why he didn't like Redux. Reducers as methods of action classes were shown to me by Scott Stoll and
Simon Lightfoot.
The Flutter packages I've authored:

async_redux
fast_immutable_collections
provider_for_redux
i18n_extension
align_positioned
network_to_file_image
image_pixels
matrix4_transform
back_button_interceptor
indexed_list_view
animated_size_and_fade
assorted_layout_widgets
weak_map
themed
bdd_framework

My Medium Articles:



Async Redux: Flutter’s non-boilerplate version of Redux (
versions:
Português)



i18n_extension (
versions:
Português)



Flutter: The Advanced Layout Rule Even Beginners Must Know (
versions: русский)



The New Way to create Themes in your Flutter App



A new BDD tool for TypeScript/React, and Flutter/Dart


My article in the official Flutter documentation:

Understanding constraints

Marcelo Glasberg:
https://github.com/marcglasberg
https://linkedin.com/in/marcglasberg
https://twitter.com/glasbergmarcelo
https://stackoverflow.com/users/3411681/marcg
https://medium.com/@marcglasberg

License:

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Customer Reviews

There are no reviews.