Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/jordan-nelson/flutter_rx

A redux style state management library for flutter apps.
https://github.com/jordan-nelson/flutter_rx

Last synced: 2 days ago
JSON representation

A redux style state management library for flutter apps.

Awesome Lists containing this project

README

        

# flutter_rx

A redux style state management library, loosely inspired by [NgRx](https://ngrx.io/) as well as [flutter_redux](https://github.com/brianegan/flutter_redux) and [redux](https://github.com/fluttercommunity/redux.dart).

## Key concepts

- **Actions** describe unique events and are typically dispatched from widgets or returned from an [Effect], such as a button click or the intent to load data from a server.
- State changes are handled by pure functions called **reducers** that take the current state and the latest action to compute a new state.
- **Selectors** are pure functions used to select, derive, and compose pieces of state.
- State is accessed with the **Store**, an observable of state and an observer of actions.
- **Effects** are used to handle any and all side effects, such as loading data from a remote server.

## Diagram

The following diagram represents the overall general flow of application state in flutter_rx.
![flutter_rx Diagram](./screenshots/Flutter_Rx.png?raw=true)

## Usage

Below is a counter app that uses flutter_rx

```dart
import 'package:flutter/material.dart';
import 'package:flutter_rx/flutter_rx.dart';
import 'package:rxdart/rxdart.dart';

/// The State of the Application.
///
/// The state must extend [StoreState], and should provide
/// overrides for [==] and [hashCode].
///
/// If a [==] override is not provided, selection memoization
/// will not work, which will lead to unnecessary rebuilds.
class AppState extends StoreState {
const AppState({required this.counter, this.isLoading = false});
final int counter;
final bool isLoading;

AppState copyWith({int? counter, bool? isLoading}) {
return AppState(
counter: counter ?? this.counter,
isLoading: isLoading ?? this.isLoading,
);
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is AppState &&
other.counter == counter &&
other.isLoading == isLoading;
}

@override
int get hashCode => counter.hashCode ^ isLoading.hashCode;
}

/// Actions describe unique events and are typically dispatched
/// from widgets or returned from an [Effect].
///
/// Actions must extend [StoreAction].
class IncrementAction extends StoreAction {
const IncrementAction();
}

/// Actions can optionally contain state.
class IncrementByAction extends StoreAction {
const IncrementByAction({required this.value});
final int value;
}

/// Actions can be used to initiate async tasks, such
/// as fetching data from a server.
class FetchCounterValueAction extends StoreAction {
const FetchCounterValueAction();
}

class FetchCounterValueSuccessAction extends StoreAction {
const FetchCounterValueSuccessAction({required this.value});
final int value;
}

/// A reducer is just a pure function that takes in a state and
/// an action, and returns a new state.
///
/// The reducers below are intended to reduce on a specific
/// action, for example [IncrementAction], but reducers can
/// also reduce on any generic [StoreAction].
AppState incrementCounterReducer(
AppState state,
IncrementAction action,
) {
return state.copyWith(counter: state.counter + 1);
}

AppState incrementCounterByReducer(
AppState state,
IncrementByAction action,
) {
return state.copyWith(counter: state.counter + action.value);
}

AppState fetchCounterValueReducer(
AppState state,
FetchCounterValueAction action,
) {
return state.copyWith(
isLoading: true,
);
}

AppState fetchCounterValueSuccessReducer(
AppState state,
FetchCounterValueSuccessAction action,
) {
return state.copyWith(
counter: action.value,
isLoading: false,
);
}

/// [createReducer] takes multiple single purpose reducers and combines them.
///
/// [On] is used to map a specific [StoreAction] to the reducer that
/// should be used when that action is dispatched.
///
/// For example, when [IncrementAction] is received, [incrementCounterReducer]
/// will be used to generate the new state.
final reducer = createReducer([
On(incrementCounterReducer),
On(incrementCounterByReducer),
On(fetchCounterValueReducer),
On(fetchCounterValueSuccessReducer),
]);

/// Effects handle any and all side effects, such as fetching data from a
/// remote server.
///
/// Effects receive the stream of actions from the store. Typically
/// this stream is filtered for a specific action.
///
/// Actions are only added to the stream that effects receive after
/// the app's reducers has processed this action and returned a new app state.
/// This guarantees that the state of the store includes mutations from the action
/// being handled by the effect.
///
/// Effects can optionally return one or more actions. These actions
/// will then be dispatched. If the effect returns a list of actions,
/// will be dispatched in the order of the list.
///
/// Effects should **not** call dispatch to dispatch new actions.
Effect onFetchCounter = (
Stream actions,
Store store,
) {
/// RxDart can be useful within effects, but does not need to be used.
return actions.whereType().flatMap((action) {
return Stream.fromFuture(fetchCounter()).map((value) {
return FetchCounterValueSuccessAction(value: value);
});
});
};

/// Mock data call.
///
/// In a real app this would be a network call.
Future fetchCounter() {
return Future.delayed(const Duration(milliseconds: 100)).then((value) => 10);
}

void main() {
/// Create your store as a final variable in the main function
/// or inside a State object.
final store = Store(
/// The initial state of the store.
initialState: const AppState(counter: 0),

/// The main reducer.
///
/// This can be a simple pure function, or composed of multiple smaller reducers.
reducer: reducer,

/// The list of effects to be registered.
effects: [onFetchCounter],
);

runApp(
/// The StoreProvider should generally be the top level widget. Only descendants
/// will have access to the StoreProvider's state.
StoreProvider(
store: store,
child: const MaterialApp(
home: HomePage(),
),
),
);
}

class HomePage extends StatelessWidget {
const HomePage({
super.key,
});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('FlutterRx Counter Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'You have pushed the button this many times:',
),

/// StoreConnector can be used to access data from the store
/// using a selector.
///
/// You do not have to use StoreConnector. Data can be streamed from the
/// store using `StoreProvider.of(context)`, and the current
/// snapshot of the store can be read with
/// `StoreProvider.of(context).value`.
StoreConnector(
/// The selector maps the apps state to a subset for use in your
/// widget.
///
/// A simple function could be used in place of createSelector,
/// but createSelector uses memoization to reduce rebuilds.
selector: createSelector((AppState state, _) => state.counter),

/// onInit can be used execute code on the first build. This is
/// often useful for dispatching an action that fetches data for
/// view.
onInit: () {
StoreProvider.of(context).dispatch(
const FetchCounterValueAction(),
);
},

/// The builder will only rebuild when when the selector emits
/// a new value, and memoization ensures that this only happens when
/// the app state has changed.
///
/// createSelector1, createSelector2, etc. can be used to compose
/// selectors together. The composed selector will only emit a new value
/// when one of the input selectors does.
builder: (BuildContext context, int value) {
return Text(
'$value',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Use the StoreProvider to get the store and call the dispatch method
// to dispatch actions.
StoreProvider.of(context).dispatch(
const IncrementAction(),
);
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
```