Flutter 应用程序架构
Riverpod 应用架构参考
使用数据层、域、应用程序和表示层的 Flutter 应用程序架构
此架构由四个不同的层组成,每个层都包含我们的应用所需的组件:
- presentation: widgets, states, and controllers
- application: services
- domain: models
- data: repositories, data sources, and DTOs (数据传输对象)
按层划分结构
‣ lib
‣ src
‣ presentation
‣ feature1
‣ feature2
‣ feature3 <--
‣ application
‣ feature1
‣ feature2
‣ feature3 <-- only create this when needed
‣ domain
‣ feature1
‣ feature2
‣ feature3 <--
‣ data
‣ feature1
‣ feature2
‣ feature3 <--
这种方法在实践中很容易使用,但随着应用程序的增长,扩展性不太好。
对于任何给定的功能,属于不同层的文件彼此相距很远。这使得开发各个功能变得更加困难,因为我们必须不断跳转到项目的不同部分。
如果我们决定要删除某个功能,则很容易忘记某些文件,因为它们都是按层组织的。
按功能划分
‣ lib
‣ src
‣ features
‣ feature1
‣ presentation
‣ application
‣ domain
‣ data
‣ feature2
‣ presentation
‣ application
‣ domain
‣ data
我发现这种方法更符合逻辑,因为我们可以轻松查看属于某个功能的所有文件,并按层分组。
优点:
- 每当我们想要添加一项新功能或修改现有功能时,我们都可以只关注一个文件夹。
- 如果我们要删除一项功能,则只需删除一个文件夹(如果我们算上相应的tests文件夹,则为两个文件夹)
功能与 UI 无关!
‣ lib
‣ src
‣ features
‣ account
‣ admin
‣ checkout
‣ leave_review_page
‣ orders_list
‣ product_page
‣ products_list
‣ shopping_cart
‣ sign_in
‣ models <-- should this go here?
‣ repositories <-- should this go here?
‣ services <-- should this go here?
不要尝试通过查看 UI 来应用功能优先方法。这将导致“不平衡”。稍后会困扰您的项目结构。
什么是“features”?
关键不在于用户看到了什么,而在于用户做了什么:
- 认证
- 管理购物车
- 查看
- 查看所有过去的订单
- 留下评论
最终确定了七个功能区域:
‣ lib
‣ src
‣ features
‣ address
‣ application
‣ data
‣ domain
‣ presentation
‣ authentication
...
‣ cart
...
‣ checkout
...
‣ orders
...
‣ products
‣ application
‣ data
‣ domain
‣ presentation
‣ admin
‣ product_screen
‣ products_list
‣ reviews
...
- 从领域层开始并确定模型类和用于操作它们的业务逻辑
- 为属于同一组的每个模型(或模型组)创建一个文件夹
- 在该文件夹中创建presentation、application、domain、data根据需要设置子文件夹
- 在每个子文件夹中,添加您需要的所有文件
构建 Flutter 应用时,UI 代码和业务逻辑之间的比例为 5:1(或更高)是很常见的。如果您的 presentation 文件夹最终包含许多文件,请不要害怕将它们分组到代表较小“子功能”的子文件夹中。
最终项目结构:
‣ lib
‣ src
‣ common_widgets
‣ constants
‣ exceptions
‣ features
‣ address
‣ authentication
‣ cart
‣ checkout
‣ orders
‣ products
‣ reviews
‣ localization
‣ routing
‣ utils
应用层
存储库模式
- 将域模型(或实体)与数据层中数据源的实现细节隔离。
- 将数据传输对象转换为领域层可以理解的经过验证的实体
- 执行数据缓存等操作。
领域模型
领域模型是包含行为和数据的领域概念模型。模型是一个简单的数据类,它无法访问属于域层之外的存储库、服务或其他对象,但是可以包含一些业务逻辑来表达它们的修改方式。为我们的业务逻辑编写单元测试不仅很容易,而且还增加了很多价值
- 用户: ID 和电子邮件
- 产品:ID、图片 URL、标题、价格、可用数量等。
- 项目:产品 ID 和数量
- 购物车:商品列表,总计
- 订单:商品列表、支付价格、状态、付款详细信息等。
表示层
widget:
- 观察状态变化,并据此重建(使用 ref.watch)
- 通过调用控制器中的方法来响应用户输入(使用 ref.read)
- 监听状态变化并在出现问题时显示错误(使用 ref.listen)
controller:
- 代表小部件与存储库交互
- 根据需要发出状态变化
Flutter Riverpod
Flutter Riverpod 是一个Flutter的状态管理库,由Remi Rousselet创建,他也是流行的provider包的作者。Riverpod旨在解决provider包的一些限制,并提供一种更灵活、更可测试的状态管理解决方案。
主要特性
- 可测试性:Riverpod旨在从一开始就易于测试。你可以独立于Flutter框架对你的状态进行测试,而不需要模拟BuildContext。
- 解耦:在provider中,状态的位置是由widget树的结构决定的。Riverpod解耦了这一点,你可以将状态的位置与其在widget树中的位置分开。
- 编译时安全:Riverpod通过提供编译时安全性来避免运行时错误。如果你尝试读取一个未被创建的状态,编译器会警告你。
- 不可变性:所有的Riverpod状态都是不可变的,这使得状态易于追踪和维护。
- 灵活性:Riverpod不依赖于BuildContext,这意味着你可以在几乎任何地方访问状态,包括异步代码中。
核心概念
- Provider:是一个描述如何创建和监听一个值的对象。这个值可以是任何东西:一个变量、一个常量、一个对象等。
- ConsumerWidget:是一个Widget,它可以监听一个或多个Provider。
- Ref:在Provider的构建方法中,你将得到一个Ref对象,它可以用来读取其他Provider,或者用于观察Provider的生命周期。
- ProviderContainer:是存储Provider状态的地方,通常是全局可用的。
- ProviderObserver:用于观察Provider的变化,这对于调试或日志记录很有用。
如何使用
- 定义Provider:首先,你需要创建一个Provider。
final myProvider = Provider((ref) => MyModel());
- 读取Provider:然后,你可以在ConsumerWidget中读取你的Provider。
class MyConsumerWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final myModel = ref.watch(myProvider);
// 使用myModel
return Text(myModel.someValue);
}
}
- 监听Provider:如果你的Provider发生变化,所有使用了watch的ConsumerWidget都会自动重建。
使用 Flutter Riverpod Generator 自动生成 Providers
"Riverpod Generator" 是一个为Riverpod提供的代码生成工具,旨在简化定义providers的语法,并增强其功能,例如允许状态热重载。这个工具可以自动为你的Riverpod providers生成类型安全的包装器和访问器,减少手动编写和维护这些样板代码的需要。
主要特性
- 自动生成代码:Riverpod Generator会为你的应用自动创建必要的provider和访问器代码,你只需定义你的业务逻辑。
- 类型安全:生成的代码是类型安全的,减少了运行时错误的可能性。
- 易于使用:与其他代码生成工具一样,Riverpod Generator集成到你的开发工作流程中,通常配合
build_runner
一起使用。 - 提高可读性:自动生成的代码遵循一定的模式,有助于保持代码的一致性和可读性。
工作原理
- 你在你的应用中定义带有特定注解的基本模型和业务逻辑。
- 使用
build_runner
运行生成器。 - 生成器读取你的代码,解析注解,并为你的Riverpod providers生成相应的代码。
如何使用
- 添加依赖:你需要在你的
pubspec.yaml
中添加riverpod_generator
和build_runner
。
dependencies:
# or flutter_riverpod/hooks_riverpod as per https://riverpod.dev/docs/getting_started
riverpod:
# the annotation package containing @riverpod
riverpod_annotation:
dev_dependencies:
# a tool for running code generators
build_runner:
# the code generator
riverpod_generator:
# riverpod_lint makes it easier to work with Riverpod
riverpod_lint:
# import custom_lint too as riverpod_lint depends on it
custom_lint:
- 启动代码生成器: 使用以下命令启动代码生成器:
dart run build_runner watch -d
,-d标志是可选的并且与 相同--delete-conflicting-outputs。顾名思义,它确保我们覆盖以前构建的任何冲突输出。 - 定义模型和逻辑:在你的代码中定义模型,使用Riverpod Generator提供的注解标记它们。
- 使用生成的代码:生成器会为你的Riverpod providers创建 *.g.dart 文件,你可以直接在你的应用中引用和使用这些文件。
示例
假设你有一个简单的模型和需要被监听的状态,你可能会这样定义:
import 'package:dio/dio.dart';
// 1. import the riverpod_annotation package
import 'package:riverpod_annotation/riverpod_annotation.dart';
// 2. add a part file
part 'dio_provider.g.dart';
// 3. use the @riverpod annotation
@riverpod
// 4. update the declaration
Dio dio(DioRef ref) {
return Dio();
}
生成 dio_provider.g.dart 文件
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'dio_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$dioHash() => r'26723d20a4ee2d05c3b01acad1196ed96cece567';
/// See also [dio].
@ProviderFor(dio)
final dioProvider = AutoDisposeProvider<Dio>.internal(
dio,
name: r'dioProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$dioHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef DioRef = AutoDisposeProviderRef<Dio>;
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
Flutter Lint & Flutter Snippets
使用
- 添加所有需要的包
pubspec.yaml
dependencies:
# the main riverpod package for Flutter apps
flutter_riverpod:
# the annotation package containing @riverpod
riverpod_annotation:
dev_dependencies:
# a tool for running code generators
build_runner:
# the code generator
riverpod_generator:
# riverpod_lint makes it easier to work with Riverpod
riverpod_lint:
# import custom_lint too as riverpod_lint depends on it
custom_lint:
- 启用custom_lint里面的插件analysis_options.yaml:
analyzer:
plugins:
- custom_lint
- 监视模式下启动 build_runner
dart run build_runner watch -d
- 安装插件 Flutter Riverpod Snippets
https://marketplace.visualstudio.com/items?itemName=robert-brunhage.flutter-riverpod-snippets