- PVSM.RU - https://www.pvsm.ru -
В конце 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.
Основой всего в Fish Redux является Component. Это объект, который состоит из трех частей: Effect, Reducer и View. Стоит отметить, что небоходимой является лишь View, т.е. Effect и Reducer описывать необязательно, компонент может работать и без них. Компонент также имеет текущее состояние (State).
Для примера возьмем кликер. Пусть в его состоянии будет лишь одно поле — count, которое будет указывать на совершённое кол-во кликов.
class ClickerState implements Cloneable<ClickerState> {
int count = 0;
@override
ClickerState clone() {
return ClickerState()
..count = count;
}
}
Состояния должны быть неизменяемыми, иммутабельными. Иммутабельность состояния можно легко поддерживать, если реализовать интерфейс Cloneable. В будущем, когда потребуется создать новое состояние, можно будет просто воспользоваться методом clone()
.
Суть редьюсера — отреагировать на какое-то действие, вернув новое состояние. Редьюсер не должен совершать никаких побочных действий.
Напишем простой редьюсер, который будет увеличивать 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 — класс в библиотеке FishRedux, который содержит два поля:
Object type
— тип экшена, обычно является объектом перечисления (enum)
dynamic payload
— параметр экшена, необязательный.
Пример:
enum Actions { increase } // перечисление с типами экшенов
class ActionsCreate { // вспомогательный класс для их создания
static Action increase(int value) => Action(Actions.increase, payload: value);
}
Логика готова, осталось отобразить результат. 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))
// увеличиваем число на единицу при нажатии на кнопку
);
}
Соберем из всего этого наш компонент:
class ClickerComponent extends Component<ClickerState> {
ClickerComponent() : super(
reducer: clickerReducer,
view: clickerView,
);
}
Как можно заметить, effect в нашем примере не используется, т.к. в нём нет необходимости. Эффект — функция, которая должна совершать все побочные действия (side effect). Но давайте придумаем случай, в котором нельзя будет обойтись без Effect. Например, таким случаем может стать повышение нашего count на случайное число с сервиса random.org.
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()
);
}
Существует расширение для 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. Давайте перейдем к главной особенности новой библиотеки — зависимостям.
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
Нажмите здесь для печати.