📜 ⬆️ ⬇️

Fish Redux - the new Redux library for Flutter

At the end of 2018, Google, with the help of the Open-Source community, made a great gift for mobile developers by releasing the first stable version of the cross-platform framework for mobile development Flutter.


However, when developing large applications, a little larger than the one-page 'Hello Worlds', the developers might have encountered the problem of uncertainty. How should I write an application? The framework is young enough, there is not yet a sufficient base of good open source examples, based on which one could understand the pros and cons of using different patterns, understand what to use in this particular case, and what doesn't.


The situation is saved by the fact that Flutter has a certain degree of similarity with React and React Native, which means you can learn some programming experience on the latter. Perhaps it was because of this that such libraries as Flutter Flux , Flutter Hooks , MobX , and also several Redux implementations appeared at once. For a long time, the most popular was a version of Brian Egan called Flutter Redux .


However, a couple of months ago, the first commit was seen by the Fish Redux library, published under the name of Alibaba. The library in a short time collected a lot of popularity, already on the first day, overtaking Brian's realization in terms of the number of stars, and on the second, twice ahead of it.


Despite its popularity, Fish has problems with documentation, which for the most part is a description of existing classes with occasional short examples. The situation is aggravated by the fact that some of the documentation is only available in Chinese. There is another difficulty: there is almost no English-speaking issue, so relying on the experience of other developers is not easy, which is very critical, considering that so far only the first previews of the version are released.


So what's the big difference between Fish and Brian's version? Flutter Redux is a state management framework. Fish is an application framework that places Redux at its center as the basis for managing a state. Those. Fish solves several more tasks and is not limited to state management .


One of the key features of Fish Redux is to unite several reducers into larger ones through direct expression of the dependencies between them, when ordinary Redux does not provide such an opportunity at all, forcing developers to implement everything on their own. But let's come back to this later, having figured out what this reducer is, as well as Fish Redux itself.


Relationship between Reducer, Effect and View in Component


image


The basis of everything in Fish Redux is Component. This is an object that consists of three parts: Effect, Reducer and View. It is worth noting that only View is visible, i.e. Effect and Reducer are optional, the component can work without them. The component also has a current state (State).

State


For example, take the clicker. Let there be only one field in its state - count, which will indicate the total number of clicks.


 class ClickerState implements Cloneable<ClickerState> { int count = 0; @override ClickerState clone() { return ClickerState() ..count = count; } } 

States must be immutable, immutable. Immunity state can be easily maintained, if you implement the interface Cloneable. In the future, when you need to create a new state, you can simply use the clone() method.


Reducer


The essence of a reducer is to respond to some action, returning a new state. The reducer should not perform any side effects.


Let's write a simple reducer that will increase the count by some number when it receives the corresponding Action (about it just below).


 ClickerState clickerReducer(ClickerState state, Action action) { //        Action,      . if (action.type == Actions.increase) { // ..       ,   ,       Count. return state.clone() ..count = state.count + action.payload; //        /payload/ count. // payload   . } // if (action.type == ...) { ... } //        . return state; } 

Also this reducer could be written in this form:


 Reducer<ClickerState> buildClickerReducer() { asReducer({ Actions.increase: (state, action) => state.clone() ..count = state.count + action.payload, //Actions.anotherAction: ... }); } 

Action


Action is a class in the FishRedux library that contains two fields:
Object type - action type, usually an enumeration object (enum)
dynamic payload - option action, optional.


Example:


 enum Actions { increase } //     class ActionsCreate { //      static Action increase(int value) => Action(Actions.increase, payload: value); } 

View


The logic is ready, it remains to display the result. View is a function that takes as parameters the current state, the dispatch function, the ViewService, and returns a widget.


The dispatch function is needed to send actions: an action, the creation of which we described earlier.
ViewService contains the current BuildContext (from the standard flutter library) and provides methods for creating dependencies, but about them later.


Example:


 Widget clickerView(ClickerState state, Dispatch dispatch, ViewService viewService) { return RaisedButton( child: Text(state.count.toString()), onPressed: () => dispatch(ActionsCreate.increase(1)) //         ); } 

Component


Let's collect from all this our component:


 class ClickerComponent extends Component<ClickerState> { ClickerComponent() : super( reducer: clickerReducer, view: clickerView, ); } 

As you can see, the effect in our example is not used, because it is not necessary. Effect - a function that must perform all side effects (side effect). But let's think up a case in which it would be impossible to do without Effect. For example, such a case could be an increase in our count by a random number from the random.org service.


Example of effect implementation
 import 'package:http/http.dart' as http; //   http   Effect<ClickerState> clickerEffect() { return combineEffects({ Actions.increaseRandomly: increaseRandomly, }); } Future<void> increaseRandomly(Action action, Context<ClickerState> context) async { final response = await http.read('https://www.random.org/integers/?num=1&min=1&max=10&col=1&base=10&format=plain'); //   random.org.      1  10. final value = int.parse(response); context.dispatch(ActionsCreate.increase(value)); } //   increaseRandomly enum Actions { increase, /* new */ increaseRandomly } class ActionsCreate { static Action increase(int value) => Action(Actions.increase, payload: value); static Action increaseRandomly() => const Action(Actions.increaseRandomly); // new } //  ,        . Widget clickerView(ClickerState state, Dispatch dispatch, ViewService viewService) { return Column( mainAxisSize: MainAxisSize.min, children: [ RaisedButton( //   child: Text(state.count.toString()), onPressed: () => dispatch(ActionsCreate.increase(1)) ), RaisedButton( //  child: const Text('Increase randomly'), onPressed: () => dispatch(ActionsCreate.increaseRandomly()) ), ] ); } //     class ClickerComponent extends Component<ClickerState> { ClickerComponent() : super( reducer: clickerReducer, view: clickerView, effect: clickerEffect() ); } 

Page


There is an extension for Component called Page <T, P>. The page includes two additional fields:
T initState(P params) is a function that returns the initial state. It will be called when creating the page.
List<Middleware<T>> middleware — List of Middleware — functions that will be called before the reducer.
And also one method:
Widget buildPage(P params) - which Widget buildPage(P params) page into a working widget.


Let's create the main page of the application:


 class MainPage extends Page<void, void> { MainPage(): super( initState: (dynamic param) {}, view: (state, dispatch, viewService) => Container(), ); } 

The page expands the component, which means it can include a reducer, effect and everything else that has a regular component.


In the example, an empty page was created that has neither state, nor reducers or effects. We will fix this later.


Flutter Redux’s Brian Egan and other Redux implementations have a little bit of this in a different form. Let's move on to the main feature of the new library - dependencies.


Depencies


Fish Redux requires explicitly defining dependencies between components. If you want to use a subcomponent in a component, then you need not only to write these two components, but also to create a connector that will be responsible for transforming one state into another. Suppose we want to embed a ClickerComponent into the MainPage page.


First you need to add our state page:


 class MainState implements Cloneable<MainState> { ClickerState clicker; @override MainState clone() { return MainState() ..clicker = clicker; } static MainState initState(dynamic params) { return MainState() ..clicker = ClickerState(); } } 

Now we can write Connector:


 class ClickerConnector extends ConnOp<MainState, ClickerState> { @override ClickerState get(MainState state) => state.clicker; //        . @override void set(MainState state, ClickerState subState) => state.clicker = subState; } 

Everything. Everything is ready to add our component:


 class MainPage extends Page<MainState, void> { MainPage(): super( initState: MainState.initState, dependencies: Dependencies( slots: { 'clicker': ClickerComponent().asDependent(ClickerConnector()), //    // 'clicker': ClickerComponent() + ClickerConnector(), }, ), view: (state, dispatch, viewService) { //   clicker-. final clickerWidget = viewService.buildComponent('clicker'); return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, children: [ Center( child: clickerWidget, //   ) ], ) ); }, ); } 

Thus, now you can build a complete working application by adding the following code to main.dart :


 void main() => runApp(MyApp()); class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { @override Widget build(BuildContext context) => MaterialApp(home: MainPage().buildPage(null)); } 

All code separated by files is available here . Have a successful development experience with Flutter.


')

Source: https://habr.com/ru/post/450586/


All Articles