fludex 0.1.0 fludex: ^0.1.0 copied to clipboard
Flutter + Redux = Fludex
Fludex 🔥 #
#
Flutter + Redux = Fludex
A redux based state managment library specialy build only for Flutter.
Why Fludex? #
- It is specialy built for Flutter and only works with Flutter.
- It makes it easy to connect to store
- It has built-in logger, thunk and futureMiddlewares
- It uses simple wrapper to rebuild UI based on store
Basics #
FludexState: #
The application state is always of type FludexState
. FludexState is implemented to make the state typesafe.
example:
FludexState<int> initState = new FludexState<int>(0);
Actions: #
Fludex has a Action
type. Any action dispatched to the store should be of type Action
.
The Action
take two arguments type
and payload
(optional). The type
defines what is the Type of Action and payload
is some additional data dispatched to store.
example:
// A normal string action
Action stringAction = new Action(type: "SOME_ACTION",payload:"SOME_PAYLOAD");
// A [FutureAction] that can only be handled by [futureMiddleware].
Future<String> future = new Future<String>.delayed(
new Duration(seconds: 5),
() => "FutureAction Resolved");
Action futureAction = new Action(type: new FutureAction<String>(future));
// A [Thunk] action that is handled by [thunk] middleware.
Thunk thunkAction = (Store store) async {
final int value =
await new Future<int>.delayed(new Duration(seconds: 3), () => 0);
store.dispatch(new Action(type: "UPDATE", payload: value));
};
Reducers: #
There is a Reducer
type that takes initState
(optional) and a StateReducer
function as argument.
A StateReducer
is a normal function that gets state
and action
as arguments, alters the state based on action and returns the new state
.
// State
FludexState<int> initState = new FludexState<int>(0);
// Reducer function of type StateReducer
FludexState reducerFunction(FludexState fludexState, Action action){
int state = fludexState.state;
if(action.type == "INC")
state++;
return new FludexState(state);
}
// Reducer
Reducer reducer = new Reducer(initState:initState,reduce:reducerFunction);
You can combine multiple reducers with built-in CombineReducer
.
CombineReducer
takes a Map<String,Reducer>
as input. When using CombineReducer
the state will be Map<String,dynamic>
and the keys of reducer's map is used to build the state.
// Reducer-1
Reducer reducer1 = new Reducer(initState:initState1,reduce:reducerFunction1);
// Reducer-2
Reducer reducer2 = new Reducer(initState:initState2,reduce:reducerFunction2);
// Root Reducer
Reducer rootReducer = new CombineReducer({
"Screen1": reducer1
"Screen2": reducer2
});
// if initState1 = 0 and initState2 = 0, after applying CombineReducer the state will be { "Screen1":0, "Screen2":0 }
Store: #
A fludex store has the following responsibilites.
- Ensure only one instance of the store exists all over the application
- Holds application state.
- Allows access to state.
- Allows to dispatch actions.
- Registers listeners via [subscribe]
You should create a store before calling runApp
.
Store
take a single argument of type Map<String,dynamic>
in which we specify the reducers and middlewares.
example:
// StateReducer
final Reducer reducer = new CombinedReducer(
{
HomeScreen.name: HomeScreen.reducer,
SecondScreen.name: SecondScreen.reducer
}
);
// Store Params
final Map<String, dynamic> params = <String, dynamic>{"reducer":reducer, "middleware": <Middleware>[logger, thunk, futureMiddleware]};
// Create the Store with params for first time.
final Store store = new Store(params);
// Run app
runApp(new MaterialApp(
home: new HomeScreen(),
routes: <String, WidgetBuilder>{
HomeScreen.name: (BuildContext context) => new HomeScreen(),
SecondScreen.name: (BuildContext context) => new SecondScreen()
},));
Once Store
created, one can easily connect to the store with StoreWrapper
. StoreWrapper
takes a builder function which is normal function that returns a Widget
connected to some state
via Store
. Once store created you can get the state by creating new instance with null
as params.
// If initState of Home is String initState = "Hello World!"
new StoreWrapper(
builder: () => new Text(new Store(null).state["Home"]);
);
Middleware: #
Middleware
is somecode put between the dispatched action and the reducer receiving the action.
Middleware
is a normal function type that receives Store
,Action
and NextDispatcher
as arguments. You can build your own middleware.
example:
// Example logger middleware
Middleware logger = (Store store, Action action, NextDispatcher next) {
print('${new DateTime.now()}: $action');
next(action);
}
Built-in Middlewares:
logger:
A built-in logger middleware logs the Action, PreviousState, NextState and TimeStamp when applied.
example logs:
I/flutter ( 3949): [INFO] Fludex Logger: {
I/flutter ( 3949): Action: FUTURE_DISPATCHED,
I/flutter ( 3949): Previous State: {HomeScreen: 0, SecondScreen: {state: Begin, count: 0, status: FutureAction yet to be dispatched, loading: false}},
I/flutter ( 3949): Next State: {HomeScreen: 0, SecondScreen: {state: Begin, count: 0, status: FutureAction Dispatched, loading: true}},
I/flutter ( 3949): Timestamp: 2017-11-09 14:33:58.935510
I/flutter ( 3949): }
I/flutter ( 3949): [INFO] Fludex Logger: {
I/flutter ( 3949): Action: FutureFulfilledAction{result: FutureAction Resolved},
I/flutter ( 3949): Previous State: {HomeScreen: 0, SecondScreen: {state: Begin, count: 0, status: FutureAction Dispatched, loading: true}},
I/flutter ( 3949): Next State: {HomeScreen: 0, SecondScreen: {state: Begin, count: 0, status: FutureAction Resolved, loading: false}},
I/flutter ( 3949): Timestamp: 2017-11-09 14:34:03.919460
I/flutter ( 3949): }
thunk:
Built-in thunk middleware that is capable of handling Thunk
actions.
A Thunk
action is just a function that takes Store
as argument.
example:
// Example Thunk action
Thunk action = (Store store) async {
final result = await new Future.delayed(
new Duration(seconds: 3),
() => "Result",
);
store.dispatch(result);
};
futureMiddleware:
A built-in futureMiddleware
that handles dispatching results of [Future] to the [Store]
The Future
or FutureAction
will be intercepted by the middleware. If the
future completes successfully, a FutureFulfilledAction
will be dispatched
with the result of the future. If the future fails, a FutureRejectedAction
will be dispatched containing the error that was returned.
example:
// First, create a reducer that knows how to handle the FutureActions:
// `FutureFulfilledAction` and `FutureRejectedAction`.
FludexState exampleReducer(FludexState fludexState,Action action) {
String state = fludexState.state;
if (action is String) {
return action;
} else if (action is FutureFulfilledAction) {
return action.result;
} else if (action is FutureRejectedAction) {
return action.error.toString();
}
return new FludexState<String>(state);
}
// Next, create a Store that includes `futureMiddleware`. It will
// intercept all `Future`s or `FutureAction`s that are dispatched.
final store = new Store(
{
"reducer": exampleReducer,
"middleware": [futureMiddleware],
}
);
// In this example, once the Future completes, a `FutureFulfilledAction`
// will be dispatched with "Hi" as the result. The `exampleReducer` will
// take the result of this action and update the state of the Store!
store.dispatch(new Future(() => "Hi"));
// In this example, the initialAction String "Fetching" will be
// immediately dispatched. After the future completes, the
// "Search Results" will be dispatched.
store.dispatch(new FutureAction(
new Future(() => "Search Results"),
initialAction: "Fetching"
));
// In this example, the future will complete with an error. When that
// happens, a `FutureRejectedAction` will be dispatched to your store,
// and the state will be updated by the `exampleReducer`.
store.dispatch(new Future.error("Oh no!"));
Example #
main function
void main(){
// StateReducer
final Reducer reducer = new CombinedReducer(
{
HomeScreen.name: HomeScreen.reducer,
SecondScreen.name: SecondScreen.reducer
}
);
// Store Params
final Map<String, dynamic> params = <String, dynamic>{"reducer":reducer, "middleware": <Middleware>[logger, thunk, futureMiddleware]};
// Create the Store with params for first time.
final Store store = new Store(params);
// Run app
runApp(new MaterialApp(
home: new HomeScreen(),
routes: <String, WidgetBuilder>{
HomeScreen.name: (BuildContext context) => new HomeScreen(),
SecondScreen.name: (BuildContext context) => new SecondScreen()
},
));
}
HomeScreen
class HomeScreen extends StatelessWidget {
//Identifier for HomeScreen
static final String name = "HomeScreen";
// Reducer for HomeScreen
static final Reducer reducer =
new Reducer(initState: initState, reduce: _reducer);
// Initial State of HomeScreen
static final FludexState<int> initState = new FludexState<int>(0);
// StateReducer function for HomeScreen
static FludexState _reducer(FludexState fludexState, Action action) {
int state_ = fludexState.state;
if (action.type == "INC") state_++;
if (action.type == "DEC") state_--;
if (action.type == "UPDATE") state_ = action.payload;
return new FludexState<int>(state_);
}
// Dispatches a "INC" action
void _incrementCounter() {
new Store(null).dispatch(new Action<String, Object>(type: "INC"));
}
// Dispatches a "DEC" action
void _decrementCounter() {
new Store(null).dispatch(new Action<String, Object>(type: "DEC"));
}
// A Thunk action that resets the state to 0 after 3 seconds
static Thunk thunkAction = (Store store) async {
final int value =
await new Future<int>.delayed(new Duration(seconds: 3), () => 0);
store.dispatch(new Action(type: "UPDATE", payload: value));
};
// Dispatches a thunkAction
void _thunkAction() {
new Store(null).dispatch(new Action(type: thunkAction));
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text("HomeScreen"),
),
body: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new StoreWrapper(
builder: () =>
new Text(new Store(null).state[HomeScreen.name].toString())),
new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new IconButton(
icon: new Icon(Icons.arrow_back),
onPressed: _decrementCounter),
new IconButton(
icon: new Icon(Icons.arrow_forward),
onPressed: _incrementCounter)
],
),
const Text(
"Dispatch a Thunk Action which resolves a future and resets the store once future resolved",
textAlign: TextAlign.center,
),
new FlatButton(
onPressed: _thunkAction,
child: const Text("Dispatch Thunk Action"))
],
),
floatingActionButton: new FloatingActionButton(
onPressed: () => Navigator.of(context).pushNamed(SecondScreen.name),
tooltip: 'Go to SecondScreen',
child: new Icon(Icons.arrow_forward),
),
);
}
}
SecondScreen
class SecondScreen extends StatelessWidget {
// Identifier for SecondScreen
static final String name = "SecondScreen";
// Reducer for SecondScreen
static final Reducer reducer =
new Reducer(initState: initState, reduce: _reducer);
// Initial state of the screen
static final FludexState<Map<String, dynamic>> initState = new FludexState<Map<String,dynamic>>(<String, dynamic>{
"state": "Begin",
"count": 0,
"status": "FutureAction yet to be dispatched",
"loading": false
});
// StateReducer function that mutates the state of the screen.
// Reducers are just functions that knows how to handle state changes and retuns the changed state.
static FludexState _reducer(FludexState _state, Action action) {
Map<String, dynamic> state = _state.state;
if (action.type == "CHANGE") {
state["state"] = "Refreshed";
state["count"]++;
} else if (action.type is FutureFulfilledAction) {
state["loading"] = false;
state["status"] = action.type
.result; // Result is be the value returned when a future resolves
Navigator.of(action.payload["context"]).pop();
} else if (action.type is FutureRejectedAction) {
state["loading"] = false;
state["status"] =
action.type.error; // Error is the reason the future failed
Navigator.of(action.payload["context"]).pop();
} else if (action.type == "FUTURE_DISPATCHED") {
state["status"] = action.payload["result"];
state["loading"] = true;
_onLoading(action.payload["context"]);
}
return new FludexState<Map<String,dynamic>>(state);
}
static void _onLoading(BuildContext context) {
showDialog<dynamic>(
context: context,
barrierDismissible: false,
child: new Container(
padding: const EdgeInsets.all(10.0),
child: new Dialog(
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const CircularProgressIndicator(),
const Text("Loading"),
],
),
),
));
}
// Dispatches a simple action with no Payload
void _change() {
new Store(null).dispatch(new Action<String, Object>(type: "CHANGE"));
}
// Builds and dispatches a FutureAction
void _delayedAction(BuildContext context) {
// A dummyFuture that resolves after 5 seconds
final Future<String> dummyFuture = new Future<String>.delayed(
new Duration(seconds: 5), () => "FutureAction Resolved");
// An Action of type [FutureAction] that takes a Future to be resolved and a initialAction which is dispatched immedietly.
final Action asyncAction = new Action(
type: new FutureAction<String>(dummyFuture,
initialAction: new Action(type: "FUTURE_DISPATCHED", payload: {
"result": "FutureAction Dispatched",
"context": context
})),
payload: {"context": context});
// Dispatching a FutureAction
new Store(null).dispatch(asyncAction);
}
// Builds a Text widget based on state
Widget _buildText1() {
final Map<String, dynamic> state = new Store(null).state[SecondScreen.name];
final String value = state["state"] + " " + state["count"].toString();
return new Container(
padding: const EdgeInsets.all(20.0),
child: new Text(value),
);
}
// Builds a Text widget based on state
Widget _buildText2() {
final bool loading = new Store(null).state[SecondScreen.name]["loading"];
return new Center(
child: new Text(
"Status: " + new Store(null).state[SecondScreen.name]["status"],
style:
new TextStyle(color: loading ? Colors.red : Colors.green),
),
);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text("SecondScreen"),
),
body: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new StoreWrapper(builder: _buildText1),
const Text(
"Dispatch a FutureAction that resolves after 5 seconds",
textAlign: TextAlign.center,
),
new StoreWrapper(builder: _buildText2),
new FlatButton(
onPressed: () => _delayedAction(context),
child: const Text("Dispatch a future action"))
],
),
floatingActionButton: new FloatingActionButton(
onPressed: _change,
tooltip: 'Refresh',
child: new Icon(Icons.refresh),
),
);
}
}
To run the example.
- git clone https://github.com/hemanthrajv/fludex.git
- cd /path to cloned dir/
- cd example
- flutter run
#
Author: #
Hemanth Raj
Built With :
Flutter - A framework for building crossplatform mobile applications.