项目作者: hemanthrajv

项目描述 :
Flutter + Redux = Fludex
高级语言: Dart
项目地址: git://github.com/hemanthrajv/fludex.git
创建时间: 2017-11-09T12:39:02Z
项目社区:https://github.com/hemanthrajv/fludex

开源协议:BSD 2-Clause "Simplified" License

下载


Fludex :fire:

#

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:

  1. 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:

  1. // A normal string action
  2. Action stringAction = new Action(type: "SOME_ACTION",payload:"SOME_PAYLOAD");
  3. // A [FutureAction] that can only be handled by [futureMiddleware].
  4. Future<String> future = new Future<String>.delayed(
  5. new Duration(seconds: 5),
  6. () => "FutureAction Resolved");
  7. Action futureAction = new Action(type: new FutureAction<String>(future));
  8. // A [Thunk] action that is handled by [thunk] middleware.
  9. Thunk thunkAction = (Store store) async {
  10. final int value =
  11. await new Future<int>.delayed(new Duration(seconds: 3), () => 0);
  12. store.dispatch(new Action(type: "UPDATE", payload: value));
  13. };

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.

  1. // State
  2. FludexState<int> initState = new FludexState<int>(0);
  3. // Reducer function of type StateReducer
  4. FludexState reducerFunction(FludexState fludexState, Action action){
  5. int state = fludexState.state;
  6. if(action.type == "INC")
  7. state++;
  8. return new FludexState(state);
  9. }
  10. // Reducer
  11. 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.

  1. // Reducer-1
  2. Reducer reducer1 = new Reducer(initState:initState1,reduce:reducerFunction1);
  3. // Reducer-2
  4. Reducer reducer2 = new Reducer(initState:initState2,reduce:reducerFunction2);
  5. // Root Reducer
  6. Reducer rootReducer = new CombineReducer({
  7. "Screen1": reducer1
  8. "Screen2": reducer2
  9. });
  10. // 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:

  1. // StateReducer
  2. final Reducer reducer = new CombinedReducer(
  3. {
  4. HomeScreen.name: HomeScreen.reducer,
  5. SecondScreen.name: SecondScreen.reducer
  6. }
  7. );
  8. // Store Params
  9. final Map<String, dynamic> params = <String, dynamic>{"reducer":reducer, "middleware": <Middleware>[logger, thunk, futureMiddleware]};
  10. // Create the Store with params for first time.
  11. final Store store = new Store(params);
  12. // Run app
  13. runApp(new MaterialApp(
  14. home: new HomeScreen(),
  15. routes: <String, WidgetBuilder>{
  16. HomeScreen.name: (BuildContext context) => new HomeScreen(),
  17. SecondScreen.name: (BuildContext context) => new SecondScreen()
  18. },));

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.

  1. // If initState of Home is String initState = "Hello World!"
  2. new StoreWrapper(
  3. builder: () => new Text(new Store(null).state["Home"]);
  4. );

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:

  1. // Example logger middleware
  2. Middleware logger = (Store store, Action action, NextDispatcher next) {
  3. print('${new DateTime.now()}: $action');
  4. next(action);
  5. }
Built-in Middlewares:
logger:

A built-in logger middleware logs the Action, PreviousState, NextState and TimeStamp when applied.

example logs:

  1. I/flutter ( 3949): [INFO] Fludex Logger: {
  2. I/flutter ( 3949): Action: FUTURE_DISPATCHED,
  3. I/flutter ( 3949): Previous State: {HomeScreen: 0, SecondScreen: {state: Begin, count: 0, status: FutureAction yet to be dispatched, loading: false}},
  4. I/flutter ( 3949): Next State: {HomeScreen: 0, SecondScreen: {state: Begin, count: 0, status: FutureAction Dispatched, loading: true}},
  5. I/flutter ( 3949): Timestamp: 2017-11-09 14:33:58.935510
  6. I/flutter ( 3949): }
  7. I/flutter ( 3949): [INFO] Fludex Logger: {
  8. I/flutter ( 3949): Action: FutureFulfilledAction{result: FutureAction Resolved},
  9. I/flutter ( 3949): Previous State: {HomeScreen: 0, SecondScreen: {state: Begin, count: 0, status: FutureAction Dispatched, loading: true}},
  10. I/flutter ( 3949): Next State: {HomeScreen: 0, SecondScreen: {state: Begin, count: 0, status: FutureAction Resolved, loading: false}},
  11. I/flutter ( 3949): Timestamp: 2017-11-09 14:34:03.919460
  12. 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:

  1. // Example Thunk action
  2. Thunk action = (Store store) async {
  3. final result = await new Future.delayed(
  4. new Duration(seconds: 3),
  5. () => "Result",
  6. );
  7. store.dispatch(result);
  8. };
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:

  1. // First, create a reducer that knows how to handle the FutureActions:
  2. // `FutureFulfilledAction` and `FutureRejectedAction`.
  3. FludexState exampleReducer(FludexState fludexState,Action action) {
  4. String state = fludexState.state;
  5. if (action is String) {
  6. return action;
  7. } else if (action is FutureFulfilledAction) {
  8. return action.result;
  9. } else if (action is FutureRejectedAction) {
  10. return action.error.toString();
  11. }
  12. return new FludexState<String>(state);
  13. }
  14. // Next, create a Store that includes `futureMiddleware`. It will
  15. // intercept all `Future`s or `FutureAction`s that are dispatched.
  16. final store = new Store(
  17. {
  18. "reducer": exampleReducer,
  19. "middleware": [futureMiddleware],
  20. }
  21. );
  22. // In this example, once the Future completes, a `FutureFulfilledAction`
  23. // will be dispatched with "Hi" as the result. The `exampleReducer` will
  24. // take the result of this action and update the state of the Store!
  25. store.dispatch(new Future(() => "Hi"));
  26. // In this example, the initialAction String "Fetching" will be
  27. // immediately dispatched. After the future completes, the
  28. // "Search Results" will be dispatched.
  29. store.dispatch(new FutureAction(
  30. new Future(() => "Search Results"),
  31. initialAction: "Fetching"
  32. ));
  33. // In this example, the future will complete with an error. When that
  34. // happens, a `FutureRejectedAction` will be dispatched to your store,
  35. // and the state will be updated by the `exampleReducer`.
  36. store.dispatch(new Future.error("Oh no!"));

Example

main function

  1. void main(){
  2. // StateReducer
  3. final Reducer reducer = new CombinedReducer(
  4. {
  5. HomeScreen.name: HomeScreen.reducer,
  6. SecondScreen.name: SecondScreen.reducer
  7. }
  8. );
  9. // Store Params
  10. final Map<String, dynamic> params = <String, dynamic>{"reducer":reducer, "middleware": <Middleware>[logger, thunk, futureMiddleware]};
  11. // Create the Store with params for first time.
  12. final Store store = new Store(params);
  13. // Run app
  14. runApp(new MaterialApp(
  15. home: new HomeScreen(),
  16. routes: <String, WidgetBuilder>{
  17. HomeScreen.name: (BuildContext context) => new HomeScreen(),
  18. SecondScreen.name: (BuildContext context) => new SecondScreen()
  19. },
  20. ));
  21. }

HomeScreen

  1. class HomeScreen extends StatelessWidget {
  2. //Identifier for HomeScreen
  3. static final String name = "HomeScreen";
  4. // Reducer for HomeScreen
  5. static final Reducer reducer =
  6. new Reducer(initState: initState, reduce: _reducer);
  7. // Initial State of HomeScreen
  8. static final FludexState<int> initState = new FludexState<int>(0);
  9. // StateReducer function for HomeScreen
  10. static FludexState _reducer(FludexState fludexState, Action action) {
  11. int state_ = fludexState.state;
  12. if (action.type == "INC") state_++;
  13. if (action.type == "DEC") state_--;
  14. if (action.type == "UPDATE") state_ = action.payload;
  15. return new FludexState<int>(state_);
  16. }
  17. // Dispatches a "INC" action
  18. void _incrementCounter() {
  19. new Store(null).dispatch(new Action<String, Object>(type: "INC"));
  20. }
  21. // Dispatches a "DEC" action
  22. void _decrementCounter() {
  23. new Store(null).dispatch(new Action<String, Object>(type: "DEC"));
  24. }
  25. // A Thunk action that resets the state to 0 after 3 seconds
  26. static Thunk thunkAction = (Store store) async {
  27. final int value =
  28. await new Future<int>.delayed(new Duration(seconds: 3), () => 0);
  29. store.dispatch(new Action(type: "UPDATE", payload: value));
  30. };
  31. // Dispatches a thunkAction
  32. void _thunkAction() {
  33. new Store(null).dispatch(new Action(type: thunkAction));
  34. }
  35. @override
  36. Widget build(BuildContext context) {
  37. return new Scaffold(
  38. appBar: new AppBar(
  39. title: const Text("HomeScreen"),
  40. ),
  41. body: new Column(
  42. mainAxisAlignment: MainAxisAlignment.center,
  43. children: <Widget>[
  44. new StoreWrapper(
  45. builder: () =>
  46. new Text(new Store(null).state[HomeScreen.name].toString())),
  47. new Row(
  48. mainAxisAlignment: MainAxisAlignment.center,
  49. children: <Widget>[
  50. new IconButton(
  51. icon: new Icon(Icons.arrow_back),
  52. onPressed: _decrementCounter),
  53. new IconButton(
  54. icon: new Icon(Icons.arrow_forward),
  55. onPressed: _incrementCounter)
  56. ],
  57. ),
  58. const Text(
  59. "Dispatch a Thunk Action which resolves a future and resets the store once future resolved",
  60. textAlign: TextAlign.center,
  61. ),
  62. new FlatButton(
  63. onPressed: _thunkAction,
  64. child: const Text("Dispatch Thunk Action"))
  65. ],
  66. ),
  67. floatingActionButton: new FloatingActionButton(
  68. onPressed: () => Navigator.of(context).pushNamed(SecondScreen.name),
  69. tooltip: 'Go to SecondScreen',
  70. child: new Icon(Icons.arrow_forward),
  71. ),
  72. );
  73. }
  74. }

SecondScreen

  1. class SecondScreen extends StatelessWidget {
  2. // Identifier for SecondScreen
  3. static final String name = "SecondScreen";
  4. // Reducer for SecondScreen
  5. static final Reducer reducer =
  6. new Reducer(initState: initState, reduce: _reducer);
  7. // Initial state of the screen
  8. static final FludexState<Map<String, dynamic>> initState = new FludexState<Map<String,dynamic>>(<String, dynamic>{
  9. "state": "Begin",
  10. "count": 0,
  11. "status": "FutureAction yet to be dispatched",
  12. "loading": false
  13. });
  14. // StateReducer function that mutates the state of the screen.
  15. // Reducers are just functions that knows how to handle state changes and retuns the changed state.
  16. static FludexState _reducer(FludexState _state, Action action) {
  17. Map<String, dynamic> state = _state.state;
  18. if (action.type == "CHANGE") {
  19. state["state"] = "Refreshed";
  20. state["count"]++;
  21. } else if (action.type is FutureFulfilledAction) {
  22. state["loading"] = false;
  23. state["status"] = action.type
  24. .result; // Result is be the value returned when a future resolves
  25. Navigator.of(action.payload["context"]).pop();
  26. } else if (action.type is FutureRejectedAction) {
  27. state["loading"] = false;
  28. state["status"] =
  29. action.type.error; // Error is the reason the future failed
  30. Navigator.of(action.payload["context"]).pop();
  31. } else if (action.type == "FUTURE_DISPATCHED") {
  32. state["status"] = action.payload["result"];
  33. state["loading"] = true;
  34. _onLoading(action.payload["context"]);
  35. }
  36. return new FludexState<Map<String,dynamic>>(state);
  37. }
  38. static void _onLoading(BuildContext context) {
  39. showDialog<dynamic>(
  40. context: context,
  41. barrierDismissible: false,
  42. child: new Container(
  43. padding: const EdgeInsets.all(10.0),
  44. child: new Dialog(
  45. child: new Row(
  46. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  47. children: [
  48. const CircularProgressIndicator(),
  49. const Text("Loading"),
  50. ],
  51. ),
  52. ),
  53. ));
  54. }
  55. // Dispatches a simple action with no Payload
  56. void _change() {
  57. new Store(null).dispatch(new Action<String, Object>(type: "CHANGE"));
  58. }
  59. // Builds and dispatches a FutureAction
  60. void _delayedAction(BuildContext context) {
  61. // A dummyFuture that resolves after 5 seconds
  62. final Future<String> dummyFuture = new Future<String>.delayed(
  63. new Duration(seconds: 5), () => "FutureAction Resolved");
  64. // An Action of type [FutureAction] that takes a Future to be resolved and a initialAction which is dispatched immedietly.
  65. final Action asyncAction = new Action(
  66. type: new FutureAction<String>(dummyFuture,
  67. initialAction: new Action(type: "FUTURE_DISPATCHED", payload: {
  68. "result": "FutureAction Dispatched",
  69. "context": context
  70. })),
  71. payload: {"context": context});
  72. // Dispatching a FutureAction
  73. new Store(null).dispatch(asyncAction);
  74. }
  75. // Builds a Text widget based on state
  76. Widget _buildText1() {
  77. final Map<String, dynamic> state = new Store(null).state[SecondScreen.name];
  78. final String value = state["state"] + " " + state["count"].toString();
  79. return new Container(
  80. padding: const EdgeInsets.all(20.0),
  81. child: new Text(value),
  82. );
  83. }
  84. // Builds a Text widget based on state
  85. Widget _buildText2() {
  86. final bool loading = new Store(null).state[SecondScreen.name]["loading"];
  87. return new Center(
  88. child: new Text(
  89. "Status: " + new Store(null).state[SecondScreen.name]["status"],
  90. style:
  91. new TextStyle(color: loading ? Colors.red : Colors.green),
  92. ),
  93. );
  94. }
  95. @override
  96. Widget build(BuildContext context) {
  97. return new Scaffold(
  98. appBar: new AppBar(
  99. title: const Text("SecondScreen"),
  100. ),
  101. body: new Column(
  102. mainAxisAlignment: MainAxisAlignment.center,
  103. children: <Widget>[
  104. new StoreWrapper(builder: _buildText1),
  105. const Text(
  106. "Dispatch a FutureAction that resolves after 5 seconds",
  107. textAlign: TextAlign.center,
  108. ),
  109. new StoreWrapper(builder: _buildText2),
  110. new FlatButton(
  111. onPressed: () => _delayedAction(context),
  112. child: const Text("Dispatch a future action"))
  113. ],
  114. ),
  115. floatingActionButton: new FloatingActionButton(
  116. onPressed: _change,
  117. tooltip: 'Refresh',
  118. child: new Icon(Icons.refresh),
  119. ),
  120. );
  121. }
  122. }

To run the example.

#

Author:

Hemanth Raj

LinkedIn

Built With :

Flutter - A framework for building crossplatform mobile applications.

References : redux.dart, redux-logger, redux-thunk & redux-future