Richard's Blog

在Flutter Firebase应用中使用Redux——体重跟踪4

字数统计: 2.6k阅读时长: 11 min
2019/02/24 Share

本文来自于我自己学习Flutter时所学习的教程的中文翻译,原文链接REDUXING FLUTTER FIREBASE APP – WEIGHTTRACKER 4

这篇文章中所使用的Firebase_auth任何版本在我的机器上都无法编译通过,考虑到上一篇对环境的要求非常严苛,以及此篇我无法正常运行应用,所以这个体重跟踪应用的系列暂时不再继续翻译

英语水平有限,内容未必准确

在这篇文章中我将展示怎样把flutter_redux库添加到我现有的与Firebase有连接的应用中。这里展示的开发内容是我的体重追踪应用(追踪体重的简单应用)的一部分,并且只有这些内容。

提示: 以下只是我对这个问题的看法。有可能有更好的,如果你有什么好的改进方法,留个言。

介绍

不久之前我偶然发现了Redux库,它能提供一种状态传递机制(不是Flutter中的state状态),这些状态能在自己创建的方法中传递。为了让你能真正明白这个概念,我建议你看看这个例子。一开始我不觉得我会需要这样的一种工具,直到我开始弄Firebase Auth和用户切换。我想在一个关于视图的类中持有一个数据库对象的引用,这个引用要对应正确的用户和附加正确的监听器,但是这行不通。特别是我还想要添加更多的Widget。所以我决定试试Redux看看它能否解决我的问题。

提示:我提到了用户切换功能,但是这个功能不在这篇文章的代码中,因为还在开发当中

开始

依赖

首先,我在pubspec.yaml添加了Redux的依赖(在2017-10-05):

1
2
3
4
5
6
dependencies:
flutter:
sdk: flutter
...
redux: ">=2.0.0 <3.0.0"
flutter_redux: 0.3.1

state和reducer

下一步是创建state类用于表示整个应用的状态(state),和操作state的stateReducer方法。在这个时候我将只有体重的列表状态。所以像这样创建redux_core.dart文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import 'package:weight_tracker/model/weight_entry.dart';

@immutable
class ReduxState {
final List<WeightEntry> entries;

ReduxState({this.entries});

ReduxState copyWith({List<WeightEntry> entries}) {
return new ReduxState(
entries: entries ?? this.entries,
);
}
}

ReduxState stateReducer(ReduxState state, action) {
//TODO: add actions
return state;
}

在这里有趣的事情是action是一个动态(dynamic)类型。因为会有不同类型的action来提供不同类型的数据,Brian Egan建议每个action都应该有一个类型,不要使用泛Action类。这也是我所做的🙂

还有个值得提到的是ReduxState类是不可变的。这表示为了“改变”state,我们不得不创建一个新的。这是为什么我要创建一个copyWith方法的原因,让state在需要改变属性时能简便的复制其他状态(state)

Store和StoreProvider

main.dart里我新加了Store对象和StoreProvider用于提供Store。从现在起,每一个StoreConnector将有可以访问store和里面的state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:weight_tracker/home_page.dart';
import 'package:weight_tracker/logic/redux_core.dart';

void main() {
runApp(new MyApp());
}

class MyApp extends StatelessWidget {
final Store store = new Store(stateReducer, initialState: new ReduxState(entries: new List()));

@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Weight Tracker',
theme: new ThemeData(
primarySwatch: Colors.green,
),
home: new StoreProvider(
store: store,
child: new HomePage(title: 'Weight Tracker'),
),
);
}
}

StoreConnector

下一步是在我的屏幕控件(screen Widget)中创建ViewModel类。现在暂时我们并不真正用到它,但是后面会非常有帮助。

1
2
3
4
5
6
@immutable
class HomePageViewModel {
final List<WeightEntry> entries;

HomePageViewModel({this.entries});
}

现在我将把我的屏幕控件(screen Widget)用StoreConnector包装起来。让我们看看是什么样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@override
Widget build(BuildContext context) {
return new StoreConnector<ReduxState, HomePageViewModel>(
converter: (store) {
return new HomePageViewModel(
entries: store.state.entries);
},
builder: (context, viewModel) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView.builder(
shrinkWrap: true,
controller: _listViewScrollController,
itemCount: viewModel.entries.length,
itemBuilder: (buildContext, index) {
//calculating difference
double difference = index == viewModel.entries.length - 1
? 0.0
: viewModel.entries[index].weight -
viewModel.entries[index + 1].weight;
return new InkWell(
onTap: () => _openEditEntryDialog(
viewModel.entries[index]),
child:
new WeightListItem(viewModel.entries[index], difference));
},
),
floatingActionButton: new FloatingActionButton(
onPressed: () => _openAddEntryDialog(),
tooltip: 'Add new weight entry',
child: new Icon(Icons.add),
),
);
},
);
}

我们可以看到主要的Widget现在是StoreConnector。它包含了两个组件:converterbuilderConverter是一个方法,它接受一个Store,返回在StoreConnector中规定的ViewModel。在这个点上,理论上我可以用一个List当成ViewModel,因为我没有用到其他的东西,不过马上会用到。另一个StoreConnector组件是builder,它也是一个方法。它非常像我们重载的build方法,除了它多了一个ViewModel的参数。我们可以使用这个ViewModel去构成我们视图中的数据。

ListView.Builder改变为使用viewModel之后,我们有了一个能很好的与state数据一起工作的ListView。现在唯一的总是是我们还没有数据。让我们添加一些!

添加action

目前为止我们那个为了改变state的reducer方法现在还空着。为了让它有作用,我们需要定义一些Action,它能让我们分辨state和定制如何改变。所以让我们说明如何添加action:

1
2
3
4
5
6
7
8
9
10
11
12
13
class AddEntryAction {
final WeightEntry weightEntry;
AddEntryAction(this.weightEntry);
}
ReduxState stateReducer(ReduxState state, action) {
if (action is AddEntryAction) {
return new state.copyWith(
entries: <WeightEntry>[]
..addAll(state.entries)
..add(action.weightEntry));
}
return state;
}

像上面这样,非常简单!现在我们只需要调用它。首先我要在ViewModel类中添加一个新的属性:

1
2
3
4
5
6
7
8
@immutable
class HomePageViewModel {
final List<WeightEntry> entries;
final Function(WeightEntry) addEntryCallback;

HomePageViewModel({this.entries,
this.addEntryCallback});
}

然后我们需要更新一下build方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@override
Widget build(BuildContext context) {
return new StoreConnector<ReduxState, HomePageViewModel>(
converter: (store) {
return new HomePageViewModel(
entries: store.state.entries,
addEntryCallback: ((entry) => store.dispatch(new AddEntryAction(entry))));
},
builder: (context, viewModel) {
return new Scaffold(
...
floatingActionButton: new FloatingActionButton(
onPressed: () => _openAddEntryDialog(viewModel.addEntryCallback),
tooltip: 'Add new weight entry',
child: new Icon(Icons.add),
),
);
},
);
}
_openAddEntryDialog(Function(WeightEntry) onSubmittedCallback) async {
WeightEntry entry = ...create entry...
if (entry != null) {
onSubmittedCallback(entry);
}
}

这里有几点值得说明:

  • 为了执行action,我们从Store对象调用dispatch方法传递我们想用的Action。这会导致reducer方法被执行然后做我们想做的事。
  • 我们可以进一步传递接受entry的方法,并在需要时精确的调用它。
  • 这个方案能清晰的展示我们的屏幕组件使用了什么数据和可以执行什么动作(action)。

现在让我们引进Firebase Database到程序里面。

将Firebase Database操作Redux化

登录

在我的想法中,我希望用户在应用启动时能马上匿名登录。为了做到这个我们需要做以下的更改:
添加新的actions

1
2
3
4
5
class InitAction {}
class UserLoadedAction {
final FirebaseUser firebaseUser;
UserLoadedAction(this.firebaseUser);
}

更新ReduxState

1
2
3
4
5
class ReduxState {
final FirebaseUser firebaseUser;
final List<WeightEntry> entries;
...
}

由于reducer不支持做任何的API调用,我们要采用Middleware。Middleware是一个为了调用一些异步方法的方法,它执行在reducer方法之前,这样reducer方法就可以没有顾虑的执行一般的方法。为了更好的理解这个概念,我建议读一下这篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
firebaseMiddleware(Store<ReduxState> store, action, NextDispatcher next) {
if (action is InitAction) {
if (store.state.firebaseUser == null) {
FirebaseAuth.instance.currentUser().then((user) {
if (user != null) {
store.dispatch(new UserLoadedAction(user));
} else {
FirebaseAuth.instance
.signInAnonymously()
.then((user) => store.dispatch(new UserLoadedAction(user)));
}
});
}
}
next(action);
}

这里就是我们试图dispatch InitAction后发生的事,然后middleware方法将试图获得正确的user,如果没有将会尝试匿名登录。重要的是InitAction里发生的事是异步的,所以reducer方法无论如何都会被执行(感谢调用next方法)。当user被获取到后,我们会dispatch包含FirebaseUser对象的新的UserLoadedAction。

接着让我们更新stateReducer方法

1
2
3
4
5
6
7
8
ReduxState stateReducer(ReduxState state, action) {
if (action is UserLoadedAction) {
return state.copyWith(firebaseUser: action.firebaseUser);
} else if (action is AddEntryAction) {
...
}
return state;
}

然后在widget中调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyApp extends StatelessWidget {
final Store store = new Store(stateReducer,
initialState: new ReduxState(
firebaseUser: null,
entries: new List()),
middleware: [firebaseMiddleware].toList());

@override
Widget build(BuildContext context) {
store.dispatch(new InitAction());
return new MaterialApp(
title: 'Weight Tracker',
theme: new ThemeData(
primarySwatch: Colors.green,
),
home: new StoreProvider(
store: store,
child: new HomePage(title: 'Weight Tracker'),
),
);
}
}

注意我们在Store初始化时加入了middleware。从现在开始,每次启动应用时都会匿名登录了。这没什么,除非我们要连接数据库。

异步使用Firebase数据库

如果你读到这里仍然什么都不明白,别灰心。在文章结尾我会解释清楚。😉

首先,让我们在state中添加DatabaseReference

1
2
3
4
5
6
class ReduxState {
final FirebaseUser firebaseUser;
final DatabaseReference mainReference;
final List<WeightEntry> entries;
...
}

然后我们创建两个新的action

1
2
3
4
5
6
7
8
9
10
11
class AddDatabaseReferenceAction {
final DatabaseReference databaseReference;

AddDatabaseReferenceAction(this.databaseReference);
}

class OnAddedAction {
final Event event;

OnAddedAction(this.event);
}

下一步我们需要更新middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
firebaseMiddleware(Store<ReduxState> store, action, NextDispatcher next) {
if (action is InitAction) {
...
} else if (action is AddEntryAction) {
store.state.mainReference.push().set(action.weightEntry.toJson());
}
next(action);
if (action is UserLoadedAction) {
store.dispatch(new AddDatabaseReferenceAction(FirebaseDatabase.instance
.reference()
.child(store.state.firebaseUser.uid)
.child("entries")
..onChildAdded
.listen((event) => store.dispatch(new OnAddedAction(event)))));
}
}

再更新reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ReduxState stateReducer(ReduxState state, action) {
if (action is AddDatabaseReferenceAction) {
return state.copyWith(mainReference: action.databaseReference);
} else if (action is OnAddedAction) {
return _onEntryAdded(state, action.event);
}
...
}

ReduxState _onEntryAdded(Event event) {
return new state.copyWith(
entries: <WeightEntry>[]
..addAll(state.entries)
..add(new WeightEntry.fromSnapshot(event.snapshot)));
}

以上代码是怎么工作的?

  • 首先我们执行InitAction,在middleware中我们获得了一个FirebaseUser对象然后执行OnUserLoadedAction
  • 然后user被更新了,我们创建DatabaseReference放进AddDatabaseReferenceAction中。
  • 这个reference也添加了OnChildAdded监听器,当被调用时会执行OnAddedAction
  • 当上面的执行完毕,就添加了一个Entry到state的列表中(就像之前AddEntryAction那样)

当执行到AddEntryAction,我们用简单的调用数据库的push方法替换了添加Entry到列表的方法。现在这个action不是被reducer处理,因为它不会影响state。

如果你对整个流程的理解还不是非常清晰,下面的UML时序图可能可以帮助你完整的了解流程(点击查看高清):

flutter_reduxing_flutter_1

为了更好的理解,我也建议获取整个源码(链接在底部)。

结语

现在我们在firebase的应用中使用了redux。我想屏幕Widget将使用明确定义的数据和action(在viewMode中),在那些widget中没有任何逻辑代码会使得widget代码更具有可读性。我鼓励你尝试在你的Flutter应用中使用Redux,它真的对整个应用有些很棒的控制。🙂

我要说的就这些!🙂

你可以在这里找到所有代码

特别鸣谢Brain Egan(@brianegan)的支持🙂

CATALOG
  1. 1. 介绍
  2. 2. 开始
    1. 2.1. 依赖
    2. 2.2. state和reducer
    3. 2.3. Store和StoreProvider
    4. 2.4. StoreConnector
  3. 3. 添加action
  4. 4. 将Firebase Database操作Redux化
    1. 4.1. 登录
    2. 4.2. 异步使用Firebase数据库
    3. 4.3. 以上代码是怎么工作的?
  5. 5. 结语