Привет всем! В этой статье я хотел бы показать вам, как создать Flutter приложение, используя Redux. Если вы не знаете, что такое Flutter, то это — SDK с открытым исходным кодом для создания мобильных приложений от компании Google. Он используется для разработки приложений под Android и iOS, а также это пока единственный способ разработки приложений под Google Fuchsia.
Если вы знакомы с Flutter и хотите создать приложение, которое хорошо спроектировано, легко тестируется и имеет очень предсказуемое поведение, — продолжайте читать данную статью и вы скоро это узнаете!
Но перед тем как мы приступим к написанию самого приложения. Давайте немного познакомимся с теорией, давайте начнем с объяснения, что такое Redux.
Что такое Redux?
Redux — это архитектура, изначально созданная для языка JavaScript и используемая в приложениях, которые созданы с использованием reactive frameworks (таких как React Native или Flutter). Redux — это упрощенная версия архитектуры Flux, созданная Facebook. По сути, вам нужно знать три вещи:
- Единственный источник правды/single source of truth - весь state вашего приложения хранится только в одном месте (называется store).
- состояние доступно только для чтения/state is read-only — для изменения state приложения необходимо отправить actions(действие), после чего создастся новый state
- изменения производятся с помощью чистых функций/pure functions — чистая функция (для простоты, это функция без side effects) принимает текущий state приложение и action и возвращает новый state приложения
Примечание: Побочный эффект функции — возможность в процессе выполнения своих вычислений: читать и модифицировать значения глобальных переменных, осуществлять операции ввода-вывода, реагировать на исключительные ситуации, вызывать их обработчики. Если вызвать функцию с побочным эффектом дважды с одним и тем же набором значений входных аргументов, может случиться так, что в качестве результата будут возвращены разные значения. Такие функции называются недетерминированными функциями с побочными эффектами.
Звучит круто, но каковы преимущества данного решения?
- у нас есть контроль над state/состоянием — это означает, что мы точно знаем, что вызвало изменение состояния, у нас нет дублированного состояния, и мы можем легко следить за потоком данных
- Reducer это чистые функции которые легко протестировать — мы можем передать state, action на вход и проверить, верность результата
- Приложение четко структурировано — у нас есть разные слои для actions, models, бизнес-логики и т. д. — так что вы точно знаете, куда добавить еще одну новую фитчу
- это отличная архитектура для более сложных приложений — вам не нужно передавать state по всему дереву вашего view от родителя к потомку
- и есть еще один ...
Redux Time Travel
В Redux возможна одна интересная возможность — Путешествие во времени! С Redux и соответствующими инструментами вы можете отслеживать state вашего приложения с течением времени, проверять фактический state и воссоздавать его в любое время. Смотрите эту возможность в действии:
Redux Widgets на простом примере
Все вышеперечисленные правила делают поток данных в Redux однонаправленным. Но что это значит? На практике все это делается с помощью actions, reducers, store и states. Давайте представим приложение, которое отображает счетчик:
- Ваше приложение имеет определенный state при старте (количество кликов, которое равно 0)
- На основании этого state/состояния отрисовывается view.
- Если пользователь нажимает на кнопку, происходит отправка action (например, IncrementCounter)
- После чего action получает reducer, который знает предыдущий state (счетчик 0), и получает action (IncrementCounter) и может вернуть новый state (счетчик теперь равен 1)
- Наше приложение имеет новый state/состояние (счетчик равен 1)
- На основании нового state, перерисовывается view, которое отобразит на экране текущий state/состояние
Итак, как вы можете видеть, как правило, это все о state. У вас есть один state всего приложения, state только read-only, и для создания нового state вам нужно отправить action. Отправка actions запускает reducer, который создает и вернет нам новый state. И история повторяется.
Давайте все же создадим небольшое приложение и более ближе познакомимся с реализацией подхода Redux в действий, приложение будет называться “Список покупок/Shopping List”
Мы посмотрим, как Redux работает на практике. Мы создадим простое приложение ShoppingCart. В приложении будут функциональные возможности, такие как:
- добавление покупок
- возможно будет пометить покупку как выполненную
- и это в основном все
Приложение будет выглядеть так:
Давайте начнем написания кода!
Необходимое условие
В этой статье я не буду показывать создание пользовательского интерфейса для данного приложения. Вы можете ознакомится с кодом который я подготовил для Вас, прежде чем продолжить погружение в Redux. После чего мы продолжим написание кода и добавление Redux в текущее приложение.
Примечание: Если вы никогда раньше не использовали Flutter, я советую вам попробовать Flutter Codelabs от Google.
Предварительная подготовка
Чтобы начать использовать Redux для Flutter, нам необходимо добавить зависимости в файл pubspec.yaml:
flutter_redux: ^0.5.2
Вы также можете проверить текущую версию данной зависимости, перейдя на страничку flutter_redux.
На момент написания статьи версия была, flutter_redux 0.6.0
Model
Наше приложение должно уметь управлять добавлением и изменением элементов, поэтому мы будем использовать простую модель CartItem для хранения состояния одного элемента. Все наше состояние приложения будет просто списком CartItems. Как видите, CartItem — это просто объект.
class CartItem {
String name;
bool checked;
CartItem(this.name, this.checked);
}
Во-первых, нам нужно объявить actions. Action — это, по сути, любое намерение, которое может быть вызвано для изменения состояния приложения. По сути нас будет два actions для добавления и изменения элемента:
class AddItemAction {
final CartItem item;
AddItemAction(this.item);
}
class ToggleItemStateAction {
final CartItem item;
ToggleItemStateAction(this.item);
}
Затем нам нужно сообщить нашему приложению, что делать с этими actions. Вот почему используются reducers — они просто принимают текущее состояние приложения и действие (application state и action), затем создают и возвращают новый state. У нас будет два reducer метода:
List<CartItem> appReducers(List<CartItem> items, dynamic action) {
if (action is AddItemAction) {
return addItem(items, action);
} else if (action is ToggleItemStateAction) {
return toggleItemState(items, action);
}
return items;
}
List<CartItem> addItem(List<CartItem> items, AddItemAction action) {
return List.from(items)..add(action.item);
}
List<CartItem> toggleItemState(List<CartItem> items, ToggleItemStateAction action) {
return items.map((item) => item.name == action.item.name ?
action.item : item).toList();
}
Метод appReducers() делегирует action соответствующим методам. Оба метода addItem() и toggleItemState() возвращают новые списки — это наш новый state/ состояние. Как видите, вы не должны изменять текущий список. Вместо этого мы каждый раз создаем новый список.
StoreProvider
Теперь, когда у нас есть actions и reducers, нам нужно предоставить место для хранения состояния приложения. В Redux он называется store и является единственным источником правды для приложения.
void main() {
final store = new Store<List<CartItem>>(
appReducers,
initialState: new List());
runApp(new FlutterReduxApp(store));
}
Чтобы создать store, нам нужно передать метод reducers и начальный state. Если мы создали store, мы должны передать его в StoreProvider, чтобы сообщить нашему приложению, что store может быть использован любым, кто захочет запросить текущий state приложения.
class FlutterReduxApp extends StatelessWidget {
final Store<List<CartItem>> store;
FlutterReduxApp(this.store);
@override
Widget build(BuildContext context) {
return new StoreProvider<List<CartItem>>(
store: store,
child: new ShoppingCartApp(),
);
}
}
В приведенном выше примере, ShoppingCartApp() является основным виджетом нашего приложения.
StoreConnector
В настоящее время у нас есть все, кроме… фактического добавления и изменения элементов для покупки. Как это сделать? Чтобы сделать это возможным, нам нужно использовать StoreConnector. Это способ получить store и отправить ему какие-то action или просто получить текущий state.
Во-первых, мы хотим получить текущие данные и отобразить их в виде списка на экране:
class ShoppingList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new StoreConnector<List<CartItem>, List<CartItem>>(
converter: (store) => store.state,
builder: (context, list) {
return new ListView.builder(
itemCount: list.length,
itemBuilder: (context, position) =>
new ShoppingListItem(list[position]));
},
);
}
}
Код выше оборачивает ListView.builder с StoreConnector. StoreConnector может принять текущий state (которое является списком элементов ) и с помощью функций map мы можем конвертировать его в что угодно. Но в нашем случае это будет один и тоже state (List ), потому что здесь нам нужен список покупок.
Далее, в функции builder мы получаем список — который в основном представляет собой список CartItems из store, который мы можем использовать для создания ListView.
Хорошо, круто — у нас есть данные. Теперь, как установить некоторые данные?
Для этого мы также будем использовать StoreConnector, но немного по-другому.
class AddItemDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new StoreConnector<List<CartItem>, OnItemAddedCallback>(
converter: (store) {
return (itemName) =>
store.dispatch(AddItemAction(CartItem(itemName, false)));
}, builder: (context, callback) {
return new AddItemDialogWidget(callback);
});
}
}typedef OnItemAddedCallback = Function(String itemName);
Давайте посмотрим на код. Мы использовали в StoreConnector, как и в предыдущем примере, но на этот раз вместо сопоставления списка CartItems с тем же списком, мы сделаем преобразование с помощью map в OnItemAddedCallback. Таким образом, мы можем передать функцию обратного вызова в AddItemDialogWidget и вызвать ее, когда пользователь будет добавлять новый элемент:
class AddItemDialogWidgetState extends State<AddItemDialogWidget> {
String itemName;
final OnItemAddedCallback callback;
AddItemDialogWidgetState(this.callback);
@override
Widget build(BuildContext context) {
return new AlertDialog(
...
actions: <Widget>[
...
new FlatButton(
child: const Text('ADD'),
onPressed: () {
...
callback(itemName);
})
],
);
}
}
Теперь каждый раз, когда пользователь нажимает кнопку «ADD», функция обратного вызова отправляет action AddItemAction().
Теперь мы можем сделать очень похожую реализацию для изменения состояния элемента.
class ShoppingListItem extends StatelessWidget {
final CartItem item;
ShoppingListItem(this.item);
@override
Widget build(BuildContext context) {
return new StoreConnector<List<CartItem>, OnStateChanged>(
converter: (store) {
return (item) => store.dispatch(ToggleItemStateAction(item));
}, builder: (context, callback) {
return new ListTile(
title: new Text(item.name),
leading: new Checkbox(
value: item.checked,
onChanged: (bool newValue) {
callback(CartItem(item.name, newValue));
}),
);
});
}
}
Как и в предыдущем примере, мы используем StoreConnector для отображения List для функции обратного вызова OnStateChanged. Теперь каждый раз, когда флажок изменяется (в методе onChanged), функция обратного вызова запускает событие ToggleItemStateAction.
Резюме
Это все! В этой статье мы создали простое приложение которое отображает список покупок и немного погрузились в использование архитектуры Redux. В нашем приложении мы можем добавить элементы и изменить их состояние. Добавление новых функций в это приложение так же просто, как добавление новых actions и reducers.
Здесь вы можете ознакомится с исходный кодом этого приложения, включая виджет Time Travel:
Надеюсь, вам понравился этот пост!