Last updated:
0 purchases
flutter rx
flutter_rx #
A redux style state management library, loosely inspired by NgRx as well as flutter_redux and redux.
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.
Usage #
Below is a counter app that uses flutter_rx
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<AppState>([
On<AppState, IncrementAction>(incrementCounterReducer),
On<AppState, IncrementByAction>(incrementCounterByReducer),
On<AppState, FetchCounterValueAction>(fetchCounterValueReducer),
On<AppState, FetchCounterValueSuccessAction>(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<AppState> onFetchCounter = (
Stream<StoreAction> actions,
Store<AppState> store,
) {
/// RxDart can be useful within effects, but does not need to be used.
return actions.whereType<FetchCounterValueAction>().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<int> 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<AppState>(
/// 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<AppState>(
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<AppState>(context)`, and the current
/// snapshot of the store can be read with
/// `StoreProvider.of<AppState>(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<AppState>(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<AppState>(context).dispatch(
const IncrementAction(),
);
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
copied to clipboard
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.