flutter_clean_architecture 包
概述
这是一个Flutter包,使得在Flutter中实现Uncle Bob的清洁架构变得简单直观。该包提供了根据清洁架构设计并针对Flutter进行调优的基本类。
安装
1. 添加依赖
在你的包的pubspec.yaml文件中添加以下内容:
dependencies:
flutter_clean_architecture: ^6.0.1
2. 安装
你可以从命令行安装包:
使用Flutter:
$ flutter packages get
或者,你的编辑器可能支持flutter packages get
。查看你的编辑器文档了解更多。
3. 导入
现在在你的Dart代码中,你可以使用:
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
Flutter清洁架构入门
介绍
这是基于Uncle Bob的书籍和博客的架构。它结合了洋葱架构和其他架构的概念。该架构的主要关注点是关注点分离和可扩展性。它由四个主要模块组成:App
、Domain
、Data
和Device
。
依赖规则
源代码依赖只指向内部。这意味着内部模块既不知道也不依赖于外部模块。但是,外部模块既知道也依赖于内部模块。外部模块代表了业务规则和策略(内部模块)运作的机制。越往内部移动,抽象程度越高。越往外部移动,具体实现越多。内部模块不知道外部模块中存在的任何类、函数、名称、库等。它们只代表规则,完全独立于实现。
层次
Domain
Domain
模块定义了应用程序的业务逻辑。它是一个独立于开发平台的模块,即它纯粹用编程语言编写,不包含平台的任何元素。在Flutter
的情况下,Domain
将纯粹用Dart
编写,不包含任何Flutter
元素。这是因为Domain
应该只关注应用程序的业务逻辑,而不是实现细节。这也允许在平台之间轻松迁移,如果出现任何问题的话。
Domain的内容
Domain
由几个部分组成。
- 实体
- 企业范围的业务规则
- 由可以包含方法的类组成
- 应用程序的业务对象
- 在整个应用程序中使用
- 当应用程序中的某些内容发生变化时,最不可能改变
- 用例
- 特定应用程序的业务规则
- 封装应用程序的所有用例
- 编排整个应用程序的数据流
- 不应受到任何UI更改的影响
- 如果应用程序的功能和流程发生变化,可能会改变
- 仓库
- 定义外层预期功能的抽象类
- 不了解外层,只定义预期功能
- 例如,
Login
用例期望有login
功能的Repository
- 例如,
- 从外层传递给
用例
Domain
代表最内层。因此,它是架构中最抽象的层。
App
App
是Domain
外部的层。App
跨越层边界与Domain
通信。然而,依赖规则从未被违反。使用多态性
,App
通过继承类与Domain
通信:实现或扩展Domain
层中存在的Repositories
的类。由于使用了多态性
,传递给Domain
的Repositories
仍然遵守依赖规则,因为就Domain
而言,它们是抽象的。实现隐藏在多态性
之后。
App的内容
由于App
是应用程序的表现层,它是最依赖框架的层,因为它包含UI和UI的事件处理程序。对于应用程序中的每个页面,App
至少定义3个类:Controller
、Presenter
和View
。
- 视图
- 只代表页面的UI。
视图
构建页面的UI,设置样式,并依赖控制器
处理其事件。视图
包含控制器
。 - 在Flutter的情况下
视图
由2个类组成- 一个继承自
View
,是代表视图
的根Widget
- 一个继承自
ViewState
,带有另一个类及其控制器
的模板特化。
- 一个继承自
ViewState
包含view
getter,这实际上是UI实现StatefulWidget
按照Flutter
的规定包含State
StatefulWidget
只用于从其他页面传递参数给State
,如标题等。它只实例化State
对象(ViewState
)并通过其消费者提供所需的控制器
。StatefulWidget
包含State
对象(ViewState
),而ViewState
包含控制器
- 总之,
StatefulWidget
和State
都由页面的View
和ViewState
表示。 ViewState
类维护一个GlobalKey
,可用作其scaffold的key。如果使用,控制器
可以通过getState()
轻松访问它以显示snackbar和其他对话框。这很有用但是可选的。
- 只代表页面的UI。
- 控制器
- 每个
ViewState
包含控制器
。控制器
提供ViewState
所需的成员数据,即动态数据。控制器
还实现ViewState
小部件的事件处理程序,但无法访问小部件本身。ViewState
使用控制器
,而不是相反。当ViewState
调用控制器
的处理程序时,可以调用refreshUI()
来更新视图。 - 每个
控制器
都继承自Controller
抽象类,该类实现了WidgetsBindingObserver
。每个控制器
类负责处理视图
的生命周期事件,可以重写:- void onInActive()
- void onPaused()
- void onResumed()
- void onDetached()
- void onDisposed()
- void onReassembled()
- void onDidChangeDependencies()
- void onInitState()
- 等..
- 此外,每个
控制器
必须 实现 initListeners() 以初始化展示器
的监听器,以保持一致性。 控制器
包含展示器
。控制器
将仓库
传递给展示器
,后者将与用例
通信。控制器
将指定展示器
应为所有成功和错误事件调用哪些监听器,如前所述。只有控制器
被允许从最外层的数据
或设备
模块获取仓库
的实例。控制器
可以访问ViewState
,并可以通过refreshUI()
刷新受控小部件
。
- 每个
- 展示器
- 每个
控制器
包含展示器
。展示器
与用例
通信,如应用
层开始时所述。展示器
将有作为函数的成员,这些函数由控制器
可选设置,并在用例
返回数据、完成或出错时被调用(如果设置了的话)。 展示器
由两个类组成展示器
例如LoginPresenter
- 包含由
控制器
设置的事件处理程序 - 包含要使用的
用例
- 使用
Observer<T>
类和适当的参数初始化并执行用例。例如,在LoginPresenter
的情况下使用username
和password
- 包含由
- 一个实现
Observer<T>
的类- 有对
展示器
类的引用。理想情况下,这应该是一个内部类,但Dart
尚不支持内部类。 - 实现3个函数
- onNext(T)
- onComplete()
- onError()
- 这3个方法代表
用例
的所有可能输出- 如果
用例
返回一个对象,它将被传递给onNext(T)
。 - 如果出错,它将调用
onError(e)
。 - 一旦完成,它将调用
onComplete()
。
- 如果
- 这些方法然后会调用
展示器
中由控制器
设置的相应方法。这样,事件就传递给了控制器
,后者可以操作数据并更新ViewState
- 有对
- 每个
- 额外
实用工具
类(任何常用函数,如时间戳获取器等)常量
类(方便使用的const
字符串)导航器
(如果需要)
数据
代表应用程序的数据层。数据
模块是最外层的一部分,负责数据检索。这可以是对服务器的API调用、本地数据库,甚至两者都有。
数据内容
- 仓库
- 每个
仓库
应该 实现 领域 层的Repository
。 - 使用
多态性
,这些来自数据层的仓库可以跨层边界传递,从视图
开始,通过控制器
和展示器
一直到用例
。 - 从数据库或其他方法检索数据。
- 负责任何API调用和高级数据操作,如
- 在数据库中注册用户
- 上传数据
- 下载数据
- 处理本地存储
- 调用API
- 每个
- 模型(不是必须的,取决于应用程序)
实体
的扩展,增加了可能依赖平台的额外成员。例如,对于本地数据库,这可以表现为本地数据库中的isDeleted
或isDirty
条目。这些条目不能出现在实体
中,因为那会违反依赖规则,因为领域不应该了解实现细节。- 在我们的应用程序中,
数据
层的模型将不是必需的,因为我们没有本地数据库。因此,我们不太可能需要在实体
中添加依赖平台的额外条目。
- 映射器
- 将
实体
对象映射到模型
,反之亦然。 - 静态类,带有静态方法,接收
实体
或模型
并返回另一个。 - 只有在存在
模型
时才需要
- 将
- 额外
- 如果需要,可以有
实用工具
类 - 如果需要,可以有
常量
类
- 如果需要,可以有
设备
作为最外层的一部分,设备
直接与平台(即Android和iOS)通信。设备
负责本机功能,如GPS
和平台本身存在的其他功能,如文件系统。设备
调用所有本机API。
数据内容
- 设备
- 类似于
数据
中的仓库
,设备
是与平台中特定功能通信的类。 - 以与
仓库
相同的方式通过层传递:使用应用
层和领域
层之间的多态性。这意味着控制器
将其传递给展示器
,然后展示器
多态地将其传递给用例
,后者将其作为抽象类接收。
- 类似于
- 额外
- 如果需要,可以有
实用工具
类 - 如果需要,可以有
常量
类
- 如果需要,可以有
用法
文件夹结构
lib/
app/ <--- 应用层
pages/ <-- 页面或屏幕
login/ <-- 应用中的某个页面
login_controller.dart <-- 登录控制器继承自`Controller`
login_presenter.dart <-- 登录展示器继承自`Presenter`
login_view.dart <-- 登录视图,2个类分别继承`View`和`ViewState`
widgets/ <-- 自定义小部件
utils/ <-- 实用函数/类/常量
navigator.dart <-- 可选的应用导航器
data/ <--- 数据层
repositories/ <-- 仓库(检索数据,heavy处理等)
data_auth_repo.dart <-- 示例仓库:处理所有认证
helpers/ <-- 任何辅助类,如http辅助类
constants.dart <-- 常量,如API密钥、路由、URL等
device/ <--- 设备层
repositories/ <--- 与平台通信的仓库,如GPS
utils/ <--- 任何实用类/函数
domain/ <--- 领域层(业务和企业)纯DART
entities/ <--- 企业实体(应用的核心类)
user.dart <-- 示例实体
manager.dart <-- 示例实体
usecases/ <--- 业务流程,如登录、登出、获取用户等
login_usecase.dart <-- 示例用例继承自`UseCase`或`CompletableUseCase`
repositories/ <--- 定义数据和设备层功能的抽象类
main.dart <--- 入口点
示例代码
View 和 ControlledWidgetBuilder
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
class CounterPage extends View {
@override
// 可以在此处注入依赖
State<StatefulWidget> createState() => CounterState();
}
class CounterState extends ViewState<CounterPage, CounterController> {
CounterState() : super(CounterController());
@override
Widget get view => MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
key: globalKey, // 使用 `View` 内置的全局键用于 scaffold 或任何其他
// 小部件为控制器提供了通过 getContext()、getState()、getStateKey() 访问它们的方法
body: Column(
children: <Widget>[
Center(
// 显示按钮被点击的次数
child: ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
return Text(controller.counter.toString());
}
),
),
// 你可以在控制器内部手动刷新
// 使用 refreshUI()
ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
return MaterialButton(onPressed: controller.increment);
}
),
],
),
),
);
}
响应式视图状态
为了处理 Flutter Web 上的屏幕,你可以利用响应式视图状态,
它抽象了主要的 Web 应用程序断点(桌面、平板和移动),以简化使用 flutter_clean_architecture
进行 Web 开发
例如:
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
class CounterPage extends View {
@override
// 可以在此处注入依赖
State<StatefulWidget> createState() => CounterState();
}
class CounterState extends ResponsiveViewState<CounterPage, CounterController> {
CounterState() : super(CounterController());
Widget AppScaffold({Widget child}) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
key: globalKey, // 使用 `View` 内置的全局键用于 scaffold 或任何其他
// 小部件为控制器提供了通过 getContext()、getState()、getStateKey() 访问它们的方法
body: child
),
);
}
@override
ViewBuilder get mobileView => AppScaffold(
child: Column(
children: <Widget>[
// 你可以在控制器内部手动刷新
// 使用 refreshUI()
ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
return Text('移动视图上的计数器 ${controller.counter.toString()}');
}
),
],
)
);
@override
ViewBuilder get tabletBuilder => AppScaffold(
child: Column(
children: <Widget>[
// 你可以在控制器内部手动刷新
// 使用 refreshUI()
ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
return Text('平板视图上的计数器 ${controller.counter.toString()}');
}
),
],
)
);
@override
ViewBuilder get desktopBuilder => AppScaffold(
child: Row(
children: <Widget>[
// 你可以在控制器内部手动刷新
// 使用 refreshUI()
ControlledWidgetBuilder<CounterController>(
builder: (context, controller) {
return Text('桌面视图上的计数器 ${controller.counter.toString()}');
}
),
],
)
);
}
具有共同控制器的小部件
如果多个小部件需要使用某个 Page
的相同 Controller
,
可以通过 FlutterCleanArchitecture.getController<HomeController>(context)
在该页面的子小部件中检索 Controller
。
例如:
import '../pages/home/home_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
class HomePageButton extends StatelessWidget {
final String text;
HomePageButton({@required this.text});
@override
Widget build(BuildContext context) {
// 使用通用控制器,假设 HomePageButton 始终是 Home 的子级
HomeController controller =
FlutterCleanArchitecture.getController<HomeController>(context);
return GestureDetector(
onTap: controller.buttonPressed,
child: Container(
height: 50.0,
alignment: FractionalOffset.center,
decoration: BoxDecoration(
color: Color.fromRGBO(230, 38, 39, 1.0),
borderRadius: BorderRadius.circular(25.0),
),
child: Text(
text,
style: const TextStyle(
color: Colors.white,
fontSize: 20.0,
fontWeight: FontWeight.w300,
letterSpacing: 0.4),
),
),
);
}
}
控制器
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
class CounterController extends Controller {
int counter;
final LoginPresenter presenter;
CounterController() : counter = 0, presenter = LoginPresenter(), super();
void increment() {
counter++;
}
/// 显示一个 snackbar
void showSnackBar() {
ScaffoldState scaffoldState = getState(); // 获取状态,在这种情况下是 scaffold
scaffoldState.showSnackBar(SnackBar(content: Text('你好')));
}
@override
void initListeners() {
// 在此初始化 presenter 监听器
// 这些将在用例执行后成功、失败或数据检索时被调用
presenter.loginOnComplete = () => print('登录成功');
presenter.loginOnError = (e) => print(e);
presenter.loginOnNext = () => print("onNext");
}
void login() {
// 在此传递适当的凭证
// 假设你有文本字段来检索它们等
presenter.login();
}
}
Presenter
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
class LoginPresenter() {
Function loginOnComplete; // 或者 `void loginOnComplete();`
Function loginOnError;
Function loginOnNext; // 在登录 presenter 的情况下不需要
final LoginUseCase loginUseCase;
// 从控制器进行依赖注入
LoginPresenter(authenticationRepo): loginUseCase = LoginUseCase(authenticationRepo);
/// 控制器调用的登录函数
void login(String email, String password) {
loginUseCase.execute(_LoginUseCaseObserver(this), LoginUseCaseParams(email, password));
}
/// 处理 [LoginUseCase] 并取消订阅
@override
void dispose() {
_loginUseCase.dispose();
}
}
/// 用于观察 [LoginUseCase] 的 `Stream` 的 [Observer]
class _LoginUseCaseObserver implements Observer<void> {
// 上面的 presenter
// 这不是最优的,但由于 Dart 的限制,这是一个变通方法。Dart 不
// 支持内部类或匿名类。
final LoginPresenter loginPresenter;
_LoginUseCaseObserver(this.loginPresenter);
/// 如果 `Stream` 发出一个值则实现
// 在这种情况下,不必要
void onNext(_) {}
/// 登录成功,在 [LoginController] 中触发事件
void onComplete() {
// 任何清理或准备工作都在这里进行
assert(loginPresenter.loginOnComplete != null);
loginPresenter.loginOnComplete();
}
/// 登录失败,在 [LoginController] 中触发事件
void onError(e) {
// 任何清理或准备工作都在这里进行
assert(loginPresenter.loginOnError != null);
loginPresenter.loginOnError(e);
}
}
UseCase
import 'package:flutter_clean_architecture/flutter_clean_architecture.dart';
// 在这种情况下,不需要参数。因此,void。否则,更改为适当的类型。
class LoginUseCase extends CompletableUseCase<LoginUseCaseParams> {
final AuthenticationRepository _authenticationRepository; // 一些需要注入的依赖
// 功能隐藏在这个
// 在 Domain 模块中定义的抽象类后面
// 它应该在 Data 或 Device 模块中实现
// 并以多态方式传递。
LoginUseCase(this._authenticationRepository);
@override
// 由于参数类型是 void,`_` 忽略参数。根据模板中使用的类型进行更改。
Future<Stream<void>> buildUseCaseStream(params) async {
final StreamController controller = StreamController();
try {
// 假设你在这里传递凭证
await _authenticationRepository.authenticate(email: params.email, password: params.password);
logger.finest('LoginUseCase 成功。');
// 触发 onComplete
controller.close();
} catch (e) {
print(e);
logger.severe('LoginUseCase 失败。');
// 触发 .onError
controller.addError(e);
}
return controller.stream;
}
}
```dart
class LoginUseCaseParams {
final String email;
final String password;
LoginUseCaseParams(this.email, this.password);
}
后台 UseCase
可以使用 BackgroundUseCase
类在单独的 isolate 上运行用例。
由于 isolate 的限制,实现这种用例与常规用例略有不同。
要创建 BackgroundUseCase
,只需扩展该类并重写 buildUseCaseTask
方法。
此方法应返回 UseCaseTask
,它只是一个返回类型为 void 且接受 BackgroundUseCaseParameters
参数的函数。
此方法应为静态方法,并将包含您希望在单独的 isolate 上运行的所有代码。此方法应使用 BackgroundUseCaseParameters
中提供的 port
与主 isolate 通信,如下所示。这个例子是执行矩阵乘法的 BackgroundUseCase
。
class MatMulUseCase extends BackgroundUseCase<List<List<double>>, MatMulUseCaseParams> {
// 必须重写
@override
buildUseCaseTask() {
return matmul; // 返回包含要在 isolate 上运行的代码的静态方法
}
/// 此方法将在单独的 isolate 上执行。[params] 包含所有数据和所需的 sendPort
static void matmul(BackgroundUseCaseParams params) async {
MatMulUseCaseParams matMulParams = params.params as MatMulUseCaseParams;
List<List<double>> result = List<List<double>>.generate(
10, (i) => List<double>.generate(10, (j) => 0));
for (int i = 0; i < matMulParams.mat1.length; i++) {
for (int j = 0; j < matMulParams.mat1.length; j++) {
for (int k = 0; k < matMulParams.mat1.length; k++) {
result[i][j] += matMulParams.mat1[i][k] * matMulParams.mat2[k][j];
}
}
}
// 将结果发送回主 isolate
// 这将转发给观察者监听器
params.port.send(BackgroundUseCaseMessage(data: result));
}
}
与常规 [UseCase] 一样,建议为任何 [BackgroundUseCase] 使用参数类。 与上面示例对应的示例如下
class MatMulUseCaseParams {
List<List<double>> mat1;
List<List<double>> mat2;
MatMulUseCaseParams(this.mat1, this.mat2);
MatMulUseCaseParams.random() {
var size = 10;
mat1 = List<List<double>>.generate(size,
(i) => List<double>.generate(size, (j) => i.toDouble() * size + j));
mat2 = List<List<double>>.generate(size,
(i) => List<double>.generate(size, (j) => i.toDouble() * size + j));
}
}
领域中的仓库
abstract class AuthenticationRepository {
Future<void> register(
{@required String firstName,
@required String lastName,
@required String email,
@required String password});
/// 使用 [username] 和 [password] 对用户进行身份验证
Future<void> authenticate(
{@required String email, @required String password});
/// 返回 [User] 是否已通过身份验证。
Future<bool> isAuthenticated();
/// 返回当前已验证的 [User]。
Future<User> getCurrentUser();
/// 重置 [User] 的密码
Future<void> forgotPassword(String email);
/// 注销 [User]
Future<void> logout();
}
此仓库应在数据层中实现
class DataAuthenticationRepository extends AuthenticationRepository {
// 单例
static DataAuthenticationRepository _instance = DataAuthenticationRepository._internal();
DataAuthenticationRepository._internal();
factory DataAuthenticationRepository() => _instance;
@override
Future<void> register(
{@required String firstName,
@required String lastName,
@required String email,
@required String password}) {
// 待实现
}
/// 使用 [username] 和 [password] 对用户进行身份验证
@override
Future<void> authenticate(
{@required String email, @required String password}) {
// 待实现
}
/// 返回 [User] 是否已通过身份验证。
@override
Future<bool> isAuthenticated() {
// 待实现
}
/// 返回当前已验证的 [User]。
@override
Future<User> getCurrentUser() {
// 待实现
}
/// 重置 [User] 的密码
@override
Future<void> forgotPassword(String email) {
// 待实现
}
/// 注销 [User]
@override
Future<void> logout() {
// 待实现
}
}
如果仓库与平台相关,请在设备层中实现。
实体
在领域层中定义。
class User {
final String name;
final String email;
final String uid;
User(this.name, this.email, this.uid);
}