Fish Redux - 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 mobile development framework Flutter.


However, when developing large applications that are slightly larger than the single-page Hello Worlds, developers might run into uncertainty. How to write an application? The framework is quite young, there is still no sufficient base of good examples with open source, based on which it would be possible to understand the pros and cons of using various patterns, to understand what should be used in this particular case and what not.


The situation is saved by the fact that Flutter has a certain degree of similarity with React and React Native, which means that you can learn from some programming experience in the latter. Perhaps because of this, libraries such as Flutter Flux , Flutter Hooks , MobX , as well as several Redux implementations appeared at once. For a long time, the most popular version was 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 gained great popularity, already on the first day ahead of Brian's implementation in terms of the number of stars, and on the second day it was two times ahead of it.


Despite its popularity, Fish has problems with documentation, which for the most part provides a description of existing classes with some short examples. To make matters worse, some documentation is only available in Chinese. There is another difficulty: there are almost no English-speaking issue, so relying on the experience of other developers is very difficult, which is very critical, given that only the first preview versions are being released.


So what is the significant difference between Fish'a version of Brian? Flutter Redux is a state management framework. Fish is an application framework that puts Redux at its center as the basis for state management. Those. Fish solves a few more tasks and is not limited only state management.


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


The relationship between Reducer, Effect and View in Component


image


The foundation 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, i.e. Effect and Reducer are optional; a component can work without them. The component also has a current state.

State


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


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

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


Reducer


The essence of the reducer is to respond to some action by returning a new state. The reducer should not commit any side effects.


We’ll write a simple reducer that will increment count by some number upon receiving the corresponding Action (about it a little lower).


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 the following form:


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

Action


Action - a class in the FishRedux library that contains two fields:
Object type- type of action, usually an enum object
dynamic payload- action parameter, 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, dispatch, 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


We will assemble our component from all this:


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

As you can see, effect is not used in our example, because it is not necessary. An effect is a function that must perform all side effects. But let's come up with a case in which you can not do without Effect. For example, this could be an increase in our count by a random number from the random.org service.


Effect implementation example
import 'package:http/http.dart' as http; // нужно добавить http в зависимости
Effect clickerEffect() {
  return combineEffects({
    Actions.increaseRandomly: increaseRandomly,
  });
}
Future increaseRandomly(Action action, Context 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 {
  ClickerComponent() : super(
    reducer: clickerReducer,
    view: clickerView,
    effect: clickerEffect()
  );
}

Page


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

Widget buildPage(P params)


Let's create the main page of the application:


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

A page extends a component, which means it can include reducer, effect, and everything else that a regular component has.


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


All this is in a slightly different form and in Brian Egan's Flutter Redux , as well as other implementations of Redux. Let's move on to the main feature of the new library - dependencies.


Dependencies


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


First you need to add the state to our page:


class MainState implements Cloneable { 
    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 { 
  @override
  ClickerState get(MainState state) => state.clicker;
  //Этот метод будет вызываться при изменении состояния дочернего компонента.
  @override
  void set(MainState state, ClickerState subState) => state.clicker = subState;
}

All. Everything is ready to add our component:


class MainPage extends Page {
    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 main.dartfollowing code:



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

All file-separated code is available here . Have a good development experience with Flutter.


Also popular now: