(译)Flutter+Redux- 如何做一个购物清单应用

原文链接

嗨,大家好!在本文中,我想向你展示如何使用Redux创建Flutter应用程序。如果你不知道Flutter是什么,我推荐你阅读我的文章Flutter - 你可能喜欢它的5个理由。但是,如果你知道Flutter是什么,并且你想要创建一个设计良好,易于测试且具有非常可预测的行为的应用程序 - 那么请继续看下去!

什么是Redux?

首先,让我们首先解释一下Redux是什么。Redux是一种应用程序架构,最初是为JavaScript构建的,现在用于使用反应式框架构建的应用程序(例如React Native或Flutter)。Redux是由Facebook制作的Flux架构的简化版本。但Redux有什么用呢?基本上,你需要知道三件事:

  1. 有一个单一的数据源-你的整个应用程序状态只保存在一个地方(被称为仓库)
  2. 状态是只读的 - 要修改应用状态,你需要发送一个action,然后新状态就被创建出来的
  3. 使用纯方法进行修改 - 纯方法(简单的说,它是没有负效应的方法),采用先前的状态和操作,并返回新状态

听起来很酷,但该解决方案的优点是什么?

  • 我们控制了状态 - 这意味着我们确切地知道导致状态变化的原因,我们没有重复状态,我们可以轻松地跟踪数据流
  • 纯reducer方法很容易对其进行测试 -我们可以传入状态和action,然后看结果是否正确
  • 应用程序结构清晰 - 我们为actionmodel,业务逻辑等提供了不同的层 - 因此你要加新功能的时候,你会知道要放在哪里。
  • 对于更复杂的应用程序来说,这是一个很棒的架构 - 您不需要将整个视图树中的状态从父级传递到子级
  • 还有一个……

Redux时间旅行

Redux中有一个很酷的功能 - 🎉时间旅行!使用Redux和适当的工具,您可以随时跟踪应用程序状态,检查实际状态并随时重新创建。看看这个功能到底是什么样的:

Redux相关Widget在一个简单例子上的应用

所有上述规则使数据在Redux是单向流动的。但是这是什么意思?事实上,这都是由多个action,多个reducer,store以及多个state完成的。让我们想象显示按钮计数器的应用程序:

  • 你的应用程序在开始的有一些状态(点击次数,即0)
  • 基于该状态视图被渲染出来了。
  • 如果用户点击按钮,则会发送action(例如IncrementCounter)
  • reducer收到动作,它知道先前的状态(计数器0),接收动作(IncrementCounter)并且可以返回新状态(计数器1)
  • 你的应用程序有了新状态(计数器1)
  • 基于新状态,视图再次被渲染出来

正如你能看到的,通常都是与状态相关的。你有单个应用程序状态,状态对于视图是只读的,并且要创建新的状态的时候你需要发送一个action。发送action会触发reducer创建并发送一个新的应用状态。循环往复。

Redux数据流

使用Redux的购物清单应用示例

让我展示一下Redux在更为复杂例子中的实践。我们将创建一个简单的购物车应用程序。在这个应用程序中,将具有以下功能:

  • 添加条目
  • 将条目标记为已选中
  • 就那么多

该应用程序的样子是这样的:


你可以在Github上看到该程序的完整代码

让我们从编码开始吧! 👇

先决条件

在本文中,我不会展示应用程序UI创建的部分。您可以查看这个实现Redux之前的Shopping List应用程序的代码。我们将从这个基础上,把Redux添加到此应用程序中。

如果你之前从未使用过Flutter,我建议您尝试使用Google推出的Flutter Codelabs

设置

要在Flutter上使用Redux运行,您需要向pubspec.yaml文件添加依赖项:

1
flutter_redux: ^0.5.2

您可以在flutter_redux包页面上查看最新版本。

模型(Model)

我们的应用程序需要管理添加和修改条目,因此我们将使用简单的CartItem模型来存储单个条目的状态。我们的整个应用程序状态将只是多个CartItem组成的列表。如你所见,CartItem只是一个普通的Dart对象

1
2
3
4
5
6
class CartItem {
String name;
bool checked;

CartItem(this.name, this.checked);
}

注意:这里是这个文件的完整源代码

Actions

首先,我们需要声明action。 Action基本上是可以被调用以更改应用程序状态的任何意图。在我们的应用程序中,我们将有两个action,用于添加和修改条目:

1
2
3
4
5
6
7
8
9
10
11
class AddItemAction {
final CartItem item;

AddItemAction(this.item);
}

class ToggleItemStateAction {
final CartItem item;

ToggleItemStateAction(this.item);
}

注意:这里是这个文件的完整源代码

Reducers

然后,我们需要告诉我们的应用程序应该如何处理这些操作。这就是Reducer的用途 - 它们只是采用当前的Stateaction,然后它们会创建并返回新的State。我们将有两种Reducer方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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()方法都返回新列表-这个列表就是我们的新应用程序状态。如你所见,您不应修改当前列表,而是每次都创建新的列表。

StoreProvider

现在,当我们有了actionreducer时,我们需要提供存储应用程序状态的位置。它在Redux中称为仓库,它是我们应用程序的唯一数据源。

1
2
3
4
5
6
7
void main() {
final store = new Store<List<CartItem>>(
appReducers,
initialState: new List());

runApp(new FlutterReduxApp(store));
}

注意:这里是这个文件的完整源代码

要创建仓库,我们需要传递reducers方法和初始应用程序状态。如果我们创建了仓库,我们必须将它传递给StoreProvider,来告诉我们的应用程序它可以被所有请求应用状态的对象使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
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()是主要的应用程序widget。

StoreConnector

目前我们拥有除了实际添加和更改条目之外的所有内容都已经做了。那么怎么添加和修改条目呢,我们需要使用StoreConnector。这是一种获取仓库并采取一些行动或读取它的状态的方法。

首先,我们想要读取当前数据并在列表中显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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]));
},
);
}
}

注意:这里是这个文件的完整源代码

上面的代码使用StoreConnector包装默认的ListView.builderStoreConnector可以获取当前应用程序状态(List)并将其与转换器方法映射到任何内容。出于这种情况的目的,它将是相同的状态(List),因为我们需要这里的整个列表。

接下来,在builder方法中我们获取到了列表 - 这基本上是来自仓库的CartItems列表,我们可以使用它来构建ListView。


好的,很酷 - 我们到读取数据这一步了。现在如何去设置一些数据?

为此,我们还将使用StoreConnector,但方式略有不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
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,就像前面的例子一样,但这一次,我们将把它映射到OnItemAddedCallback,而不是将CartItems列表映射到同一个列表中。这样我们就可以将回调传递给AddItemDialogWidget,并在用户添加一些新项时调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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”按钮时,回调都会发送AddItemAction()事件。

现在,我们可以为切换条目状态做类似的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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));
}),
);
});
}
}

typedef OnStateChanged = Function(CartItem item);

注意:这里是这个文件的完整源代码

与前面的示例一样,我们使用StoreConnector将List映射到OnStateChanged回调。现在每次更改复选框(在onChanged方法中),回调都会触发ToggleItemStateAction事件。

摘要

以上就是全部内容!在本文中,我们使用Redux架构创建了一个简单的购物清单应用程序。在我们的应用程序中,我们可以添加一些条目并更改其状态。向此应用程序添加新功能就像添加新action和reducer一样简单。

在这里,您可以查看此应用程序的完整源代码,包括时间旅行widge:

希望你喜欢这篇文章并敬请期待更多! 🙌

非典型前端coder wechat
想要随时Follow我的最新博客,可扫码关注我的公众号