看 Flutter 架构示例有感

本文最后更新于 2021年4月4日 晚上

整个 Flutter 开发都围绕着 APP 状态管理来展开, 看了 Github 上的 Flutter 架构示例库, 并对库进行分析整理, 形成这篇文章.

此外在这篇文章中还记录有一些小技巧.

架构示例分析

架构示例库是一个起步, 可以发现很多比较优秀的实践方案, 其中实现的是相同的 APP, 但使用不同的架构来实现状态管理.

库中有如下的例子:

  1. Lifting State Up (Vanilla) Example : 只使用 Flutter 提供的设施来进行 APP 的状态管理.
  2. InheritedWidget Example: 将状态放到顶层, 让整个结构中都可以获取到状态.
  3. BLoC Example: 使用业务逻辑组件来实现状态管理.
  4. “Simple” BLoC Example: 使用 Function 和流来简化 BLoC 的实现.
  5. Redux Example: Redux 实现状态管理
  6. built_redux Example: 使用 built_redux 库实现的 Redux 状态管理.(这个库可以暂不关注)
  7. scoped_model Example: 使用 scoped_model 库来实现状态管理和状态更新.(该库实现了一个 “响应式” 的状态管理模式, 即状态改变后可以引起 Widget 重建.), 关于 ScopedModel 可以参考这篇文章.
  8. Firestore Redux Example: 只用到了 Redux 在 Flutter 上的实现库 Redux 来进行状态管理, 然后替换了一下持久化工具.
  9. MVU Example: 使用 MVU 模式来实现状态管理, 不过这个比较冷门, 可以暂时不关注.
  10. ReduRx Example: 使用 Redux + Rx 进行状态管理.
  11. MVI Example: 这个概念从来没有接触过, 可以看看
  12. MVC Example: 传统 MVC 的实现.

此外, 可以看看 github 客户端那个库:

  1. 使用 redux + flutter_redux 在其中实现了一套 Redux 的管理状态的套路以及处理逻辑.

状态提升方式管理状态

runApp 开始: VanillaApp –> VanillaAppState –> MaterialApp

在顶层 App widget(即根 Widget, 一般为 StatefulWidget 类型) 关联的 State 对象内持有当前 App 的全部状态信息:

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
/// 持有 appstate
class _VanillaAppState extends State<VanillaApp> {
/// app 状态, 初始状态是 "加载中"
AppState appstate = AppState.isLoading();

@override
void initState() {
super.initState();
// 创建 Widget State 时, 从 widget 的 repository 中加载数据(异步)
// 由于外界(比如 runApp 函数)只能使用 Widget, 故需要 widget 去持有
// repository 和 webClient 等工具.
widget.repository.loadTodos().then((loadedTodos) {
setState(() {
// 更新状态
appState = AppState(
todos: loadedTodos.map(Todo.fromEntity).toList(),
);
});
}).catchError((err) {
setState(() {
// 结束加载状态
appState.isLoading = false;
});
});
}

@override
Widget build(BuildContext context) {
//...
}
}

在 initState 里面去加载初始状态, 比如可以读取持久化的一些信息, 然后用来更新显示.

实际上StatefulWidget的刷新和相关数据分量的改变还是整体的替换是无关的, 而是 setState 决定的, 调用 setState 后, 全部数据都是去原来的位置读一遍然后重建树的. 数据改变了的当然就重建了, 没有改变的就不动罢了.

孩子里面如何更新状态?

答: 孩子里面持有从父传入的回调块, 在回调中传入子中的数据, 从而更新状态, 状态更新后, 在整个 App 范围内使用该状态的位置都可以获取到更新.

孩子里面如何获取到状态?

状态是被从顶层沿路注入下去的.

这种做法实际和 Redux 的集中状态管理 Store 有些类似, 不过需要进行注入这个就有点蛋疼了, 而且是一层一层往下传递的. Redux 强化了使用 action 更新 Store 的套路, 这样不用担心不小心改变到 State. 故实际 Redux 比这个更实用, 且回调块也需要不断往下传递…

小工程可以进行这样的实践, 仍然存在一个代码量增大的问题, 如果不进行注入, 而是直接全局访问, 这样可以减少代码量, 但无法保证在顶层进行 setState 从而执行树的重建.

但那个 3 万行的工程是怎么使用这样的实践来完成迁移的呢? 这个就要看下一小节介绍的加强版状态提升了.

使用 InheritedWidget 方式: 加强版状态提升

InheritedWidget

Base class for widgets that efficiently propagate information down the tree.

由于状态提升方式有诸多不便, 故寻找另外的更好的方式, 比如这里的 InheritedWidget, 它是抽象类.

用到 InheritedWidget 在根节点或上层结点, 在子结点中获取顶层状态容器就非常简单了, 且获取到状态容器后再在上面调用会引起 setState 的方法就可以更新状态和显示了.

整体结构是这样的:

runApp : StateContainer(StatefulWidget) –> StateContainerState(其中持有 APP 状态对象, 并在其中提供状态改变方法) –> _InheritedStateContainer(继承自 InheritedWidget ) –> InheritedWidgetApp (StatelessWidget) –> MaterialApp –> home –> 其他 route

而在 MaterialApp 结点的子结点中想要获取 state, 可以直接调用顶层 StateContainer 的如下方法:

1
2
3
4
5
static StateContainerState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(_InheritedStateContainer)
as _InheritedStateContainer)
.data;
}

这个方法也是在使用 InheritedWidget 时约定的方法, 这样子结点中就可以获取到 _InheritedStateContainer 对象的 data, 而这个 data 实际上就是我们故意将顶层的 StateContainerState 对象传递进去的.

这样获取到 StateContainerState 后就可以调用它上面定义的状态改变方法来改变状态, 同时也可以获取到各种所需的状态信息了. 也就避免了普通状态提升管理时候的的那种层层注入操作.

比如在某个 route 中这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取状态容器和状态信息
final container = StateContainer.of(context);
final state = container.state;

// 读取状态信息:
final allComplete = state.allComplete,

// 调用改变状态方法:
container.toggleAll();

// 其中 toggleAll 方法的实现如下(是 StateContainer 类中的对象方法):
void toggleAll() {
setState(() {
state.toggleAll();
});
}

这个办法非常简单且实用.

具体的分析可以看看这篇文章.

简单版 BLoc

依赖了 rx, 并且配合异步 api : Future 和 Stream, 这篇文章有比较详细的描述, 需要整理.

关于流的更多内容, 在这篇文章, 和这篇文章中, 需要详细看看.

这个例子里用到了带参数的 main, 而且是自定义类型的参数, 故看是个什么机制?

不是一种特殊机制, 而只是如下的组织:

main.dart

1
2
3
4
5
6
void main({
@required TodosInteractor todosInteractor,
@required UserRepository userRepository,
}) {
// ...
}

main_firebase.dart

1
2
3
4
5
6
7
8
9
10
11
import 'main.dart' as app;

void main() {
// 调用带参数的 main
app.main(
todosInteractor: TodosInteractor(
FirestoreReactiveTodosRepository(Firestore.instance),
),
userRepository: FirebaseUserRepository(FirebaseAuth.instance),
);
}

main_local_storage.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'main.dart' as app;

void main() {
// 调用带参数 main
app.main(
todosInteractor: TodosInteractor(
ReactiveTodosRepositoryFlutter(
repository: TodosRepositoryFlutter(
fileStorage: FileStorage(
'__bloc_local_storage',
getApplicationDocumentsDirectory,
),
),
),
),
userRepository: AnonymousUserRepository(),
);
}

在运行的时候, 选择执行对应的 main_<*> 文件即可.

这种方式可能会成为多版本编译的手段.

而 BLoC 就是 Business Logic Component 的缩写, 使用一些业务逻辑组件将视图和业务完全分离, 实际上就是对清晰架构的一种另类实践罢了, 核心思想就是把业务放到和视图分离的类中实现, 然后通过业务接口来隔离业务和视图, 并且视图完全不必关注状态管理.

Redux 版本

在 Redux 版本中, 需要把应用状态设置为不可变的. 因为每次都会生成一个新的状态.

且有一个地方: APP 状态中记录有 APP 中的所有状态信息, 包括界面显示状态(显示信息)和数据状态(数据信息), 比如当前处于哪个 Tab, 当前列表中对应的数据等.

具体的细节可以参考 github 客户端库里面的实现.

一些小技巧记录

  1. 重写 hash 和 equal 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @override
    bool operator ==(other) {
    return identical(this, other) ||
    (this.runtimeType == other.runtimeType &&
    other is Todo &&
    this.id == other.id &&
    this.complete == other.complete &&
    this.note == other.note &&
    this.task == other.task);
    }

    @override int get hashCode {
    return complete.hashCode ^ id.hashCode ^ note.hashCode ^ task.hashCode;
    }

    其中 identical 函数用于判断两个引用是否相等, 另外在判等的时候还需要判断具体类型是否相等, 这里使用的是 runtimeType 属性来判断.

    使用异或(^)操作符来生成 hash 的弊端就是如果有两个字符串的值是交替的, 则仍然可能是判断为相同. 故这里加入了一个 id(UUID) 来唯一标识对象.

    在一段连续的表达式中, 如果要对某个对象进行转型, 则直接在某个位置使用 is 操作符, 则后续的操作中该对象引用都是这个类型的了.

  2. 构造方法中:

    1
    2
    Todo(this.task, {this.complete = false, this.note = '', String id})
    : this.id = id ?? Uuid().generateV4();

    仍然有一个既要判断是否为 null 又要判断是否为空的需求, 则应该怎么处理?

    可以这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Todo(this.task, {this.complete = false, this.note = '', String id})
    : this.id = _validID(id);

    static String _validID(String id) {
    if (id == null || id.isEmpty) {
    return Uuid().generateV4();
    }
    return id;
    }
  3. 工厂方法:

    1
    2
    3
    4
    5
    6
    7
    factory AppState.isLoading() => AppState(isLoading: true);

    // 和如下等价

    factory AppState.isLoading() {
    return AppState(isLoading: true);
    }
  4. 计算属性:

    1
    bool get isAllComplete => todoList.every((todo) => todo.complete);
  5. 全部对象的选择和反选技巧:

    1
    2
    3
    4
    5
    void toggleAll() {
    final allCompleted = this.allComplete;

    todos.forEach((todo) => todo.complete = !allCompleted);
    }

    首先获取是否全选, 未全选就全选. 这样的写法是最简单的一种.

  6. 一种 UUID 生成算法:

    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 Uuid {
    final Random _random = Random();

    /// Generate a version 4 (random) uuid. This is a uuid scheme that only uses
    /// random numbers as the source of the generated uuid.
    String generateV4() {
    // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12.
    final int special = 8 + _random.nextInt(4);

    return '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-'
    '${_bitsDigits(16, 4)}-'
    '4${_bitsDigits(12, 3)}-'
    '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-'
    '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}';
    }

    String _bitsDigits(int bitCount, int digitCount) =>
    _printDigits(_generateBits(bitCount), digitCount);

    int _generateBits(int bitCount) => _random.nextInt(1 << bitCount);

    String _printDigits(int value, int count) =>
    value.toRadixString(16).padLeft(count, '0');
    }
  7. 可选类型的实现:

    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
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
     contains a value.
    bool get isPresent => _value != null;

    /// Whether the Optional contains a value.
    bool get isNotPresent => _value == null;

    /// Gets the Optional value.
    ///
    /// Throws [StateError] if [value] is null.
    T get value {
    if (this._value == null) {
    throw StateError('value called on absent Optional.');
    }
    return _value;
    }

    /// Executes a function if the Optional value is present.
    void ifPresent(void ifPresent(T value)) {
    if (isPresent) {
    ifPresent(_value);
    }
    }

    /// Execution a function if the Optional value is absent.
    void ifAbsent(void ifAbsent()) {
    if (!isPresent) {
    ifAbsent();
    }
    }

    /// Gets the Optional value with a default.
    ///
    /// The default is returned if the Optional is [absent()].
    ///
    /// Throws [ArgumentError] if [defaultValue] is null.
    T or(T defaultValue) {
    if (defaultValue == null) {
    throw ArgumentError('defaultValue must not be null.');
    }
    return _value == null ? defaultValue : _value;
    }

    /// Gets the Optional value, or [null] if there is none.
    T get orNull => _value;

    /// Transforms the Optional value.
    ///
    /// If the Optional is [absent()], returns [absent()] without applying the transformer.
    ///
    /// The transformer must not return [null]. If it does, an [ArgumentError] is thrown.
    Optional<S> transform<S>(S transformer(T value)) {
    return _value == null
    ? Optional.absent()
    : Optional.of(transformer(_value));
    }

    @override
    Iterator<T> get iterator =>
    isPresent ? <T>[_value].iterator : Iterable<T>.empty().iterator;

    /// Delegates to the underlying [value] hashCode.
    int get hashCode => _value.hashCode;

    /// Delegates to the underlying [value] operator==.
    bool operator ==(o) => o is Optional && o._value == _value;

    String toString() {
    return _value == null
    ? 'Optional { absent }'
    : 'Optional { value: ${_value} }';
    }
    }
  8. 关于分页视图的分析可以看这一篇, 里面有很多实用效果的实现!!

  9. 滑动动画实现这篇文章, 把闲鱼那种动画也实现了, 看这篇非常有必要!!!!

  10. 尼玛遍地都是那种效果, 为什么 iOS 端就没有说明实现方法的呢? 这个是在 Flutter 中的实现, 这篇文章值得看.

  11. 底部导航的动画点击效果实现可以参考这篇文章.

  12. 关于 Flutter 的优劣分析, 以及 使用 Flutter 一年的感受.

  13. 可以针对每个不同方面建立一个问答, 收集解答方案.

  14. 需要探索在现有 APP 中插入 Flutter 的方法, 跟着官方文档走, 目前实践中大多都是这样做的.

  15. 目前 flutter 升级是在 stable channel 上的, 在 dev 上已经升级到了 1.1.7, 故可以尝试一下切换到 dev channel 更新:

    1
    2
    flutter channel dev
    flutter upgrade

    如果后续要修改, 也是切换到 stable 频道然后更新一次就可以了.

  16. 多版本的实现可以参考这篇文章, 果然就是用的不同的main.


看 Flutter 架构示例有感
https://blog.rayy.top/2019/01/12/2019-35-flutter-architecture-record/
作者
貘鸣
发布于
2019年1月12日
许可协议