- PVSM.RU - https://www.pvsm.ru -

Fish Redux — новая Redux библиотека для Flutter

В конце 2018 года Google, не без помощи Open-Source сообщества, сделал большой подарок для мобильных разработчиков, выпустив первую стабильную версию кросс-платформенного фреймворка для мобильной разработки Flutter.

Однако, при разработке крупных приложений, немного больших, чем одностраничные 'Hello World'ы, разработчики могли столкнуться с проблемой неопределенности. Как стоит писать приложение? Фреймворк достаточно молод, ещё не существует достаточной базы хороших примеров с открытым кодом, основываясь на которых можно было бы понять плюсы и минусы применения различных паттернов, понять что стоит использовать в данном конкрентном случае, а что — нет.

Спасает ситуацию то, что Flutter имеет определенную степень схожести с React и React Native, а значит можно перенять некоторый опыт программирования на последних. Возможно, именно из-за этого появились такие библиотеки, как Flutter Flux [1], Flutter Hooks [2], MobX [3], а также сразу несколько реализаций Redux. Долгое время самой популярной была версия Брайана Игана под названием Flutter Redux [4].

Тем не менее, пару месяцев назад первый коммит увидела библиотека Fish Redux [5], опубликованная под именем компании Alibaba. Библиотека за короткое время собрала большую популярность, уже на пером дне обогнав реализацию Брайана по колличеству звезд, а на втором опередив её в два раза.

Несмотря на популярность, у Fish присутсвуют проблемы с документацией, которая по большей части представляет описание существующих классов с иногда встречающимися короткими примерами. Усугубляет ситуацию то, что некоторая часть документации доступна только на китайском языке. Есть и другая сложность: англоязычных issue почти не существует, соотвественно полагаться на опыт других разработчиков весьма непросто, что бывает весьма критично, учитывая то, что пока выходят лишь первые превью версии.

Так в чём же существенное отличие Fish'a от версии Брайана? Flutter Redux — фреймворк, отвечающий за управление состоянием. Fish является фреймворком приложения, ставящий в его центр Redux, как основу для управления состояния. Т.е. Fish решает несколько больше задач и не ограничивается лишь state management.

Одной из ключевых особенностей Fish Redux является объединение нескольких редьюсеров в более крупные через прямое выражение зависимости между ними, когда обычный Redux не предоставляет такую возможность вообще, заставляя разработчиков реализовывать всё своими силами. Но давайте вернемся к этому позже, разобравшись с тем, что же из себя представляет этот редьюсер, а также сам Fish Redux.

Связь между Reducer, Effect и View в Component

image

Основой всего в Fish Redux является Component. Это объект, который состоит из трех частей: Effect, Reducer и View. Стоит отметить, что небоходимой является лишь View, т.е. Effect и Reducer описывать необязательно, компонент может работать и без них. Компонент также имеет текущее состояние (State).

State

Для примера возьмем кликер. Пусть в его состоянии будет лишь одно поле — count, которое будет указывать на совершённое кол-во кликов.

class ClickerState implements Cloneable<ClickerState> { 
    int count = 0;

    @override
    ClickerState clone() {
        return ClickerState()
            ..count = count;
    }
}

Состояния должны быть неизменяемыми, иммутабельными. Иммутабельность состояния можно легко поддерживать, если реализовать интерфейс Cloneable. В будущем, когда потребуется создать новое состояние, можно будет просто воспользоваться методом clone().

Reducer

Суть редьюсера — отреагировать на какое-то действие, вернув новое состояние. Редьюсер не должен совершать никаких побочных действий.

Напишем простой редьюсер, который будет увеличивать count на какое-то число при получении соответствующего Action'a (про него чуть ниже).

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;
}

Также данный редьюсер можно было записать в таком виде:

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

Action

Action — класс в библиотеке FishRedux, который содержит два поля:
Object type — тип экшена, обычно является объектом перечисления (enum)
dynamic payload — параметр экшена, необязательный.

Пример:

enum Actions { increase } // перечисление с типами экшенов
class ActionsCreate {  // вспомогательный класс для их создания
    static Action increase(int value) => Action(Actions.increase, payload: value);
}

View

Логика готова, осталось отобразить результат. View является функцией, принимающей как параметры текущее состояние, функцию dispatch, ViewService и возвращающей Widget.

Функция dispatch нужна для отправки действий: экшена, создание которого мы описали раньше.
ViewService содержит текущий BuildContext (из стандартной библиотеки flutter) и предоставляет методы для создания зависимостей, но про них позже.

Пример:

Widget clickerView(ClickerState state, Dispatch dispatch, ViewService viewService) { 
    return RaisedButton(
        child: Text(state.count.toString()),
        onPressed: () => dispatch(ActionsCreate.increase(1))
        // увеличиваем число на единицу при нажатии на кнопку
    );
}

Component

Соберем из всего этого наш компонент:

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

Как можно заметить, effect в нашем примере не используется, т.к. в нём нет необходимости. Эффект — функция, которая должна совершать все побочные действия (side effect). Но давайте придумаем случай, в котором нельзя будет обойтись без Effect. Например, таким случаем может стать повышение нашего count на случайное число с сервиса random.org.

Пример реализации effect'а

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

Существует расширение для Component под названием Page<T, P>. Страница включает в себя два дополнительных поля:
T initState(P params) — функция, возвращающая начальное состояние. Будет вызвана при создании страницы.
List<Middleware<T>> middleware — список Middleware — функций, которые будут вызваны перед редьюсером.
А также один метод:
Widget buildPage(P params) — который собирает страницу в рабочий виджет.

Давайте создадим главную страницу приложения:

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

Страница расширяет компонент, а значит может включать в себя reducer, effect и всё остальное, что имеет обычный компонент.

В примере была создана пустая страница, которая не имеет ни состояния, ни редьюсеров или эффектов. Позднее мы исправим это.

Всё это немного в другом виде есть и в Flutter Redux [4] Брайана Игана, а также других реализациях Redux. Давайте перейдем к главной особенности новой библиотеки — зависимостям.

Dependencies

Fish Redux требует явно определять зависимости между компонентами. Если вы хотите использовать подкомпонент в компоненте, то нужно не только написать эти два компонента, но и создать коннектор, который будет ответственен за преобразование одного состояния в другое. Предположим, что мы хотим встроить ClickerComponent на страницу MainPage.

Для начала нужно добавить нашей странице состояние:

class MainState implements Cloneable<MainState> { 
    ClickerState clicker;

    @override
    MainState clone() {
        return MainState()
            ..clicker = clicker;
    }

    static MainState initState(dynamic params) { 
        return MainState()
            ..clicker = ClickerState();
    }
}

Теперь мы можем написать Connector:

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

  //Этот метод будет вызываться при изменении состояния дочернего компонента.
  @override
  void set(MainState state, ClickerState subState) => state.clicker = subState;
}

Всё. Всё готово для добавления нашего компонента:

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, // отображаем его
                      )
                    ],
                  )
                );
            },
        );
}

Таким образом, теперь можно собрать полное рабочее приложение, добавив в 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));
}

Весь код, разделенный по файлам, доступен здесь [6]. Удачного опыта разработки с Flutter.

Автор: Festelo

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/dart/316621

Ссылки в тексте:

[1] Flutter Flux: https://github.com/google/flutter_flux

[2] Flutter Hooks: https://github.com/rrousselGit/flutter_hooks

[3] MobX: https://github.com/mobxjs/mobx.dart

[4] Flutter Redux: https://github.com/brianegan/flutter_redux

[5] Fish Redux: https://github.com/alibaba/fish-redux

[6] Весь код, разделенный по файлам, доступен здесь: https://github.com/festelo/fish-clicker-example

[7] Источник: https://habr.com/ru/post/450586/?utm_source=habrahabr&utm_medium=rss&utm_campaign=450586