前言
对于Flutter在逐渐的熟悉,基本经历的几个阶段
- BLoC pattern 试着使用Dart语言的 Stream 去做些刷新处理(很多入门书也是这么说的)
- Provider 挺方便好用的库,正在使用
- Clean Architecture 正在想实践一波的东西,当然和看的《架构整洁之道》有关
- 随着不断的深入,发现怎么 ViewModel 爆炸了,Native时良好的Clean架构都没了。。。。。
- 就想着再分离出 domain、data 层来
- 《架构整洁之道》绝对要推荐一波,怎样去评价组件的好坏、组件的发展周期、SOLID重新回顾、引出 clean架构、编程几十年也不会变的编程范式

《架构整洁之道》
整本书看过一遍,但也难以完全理解,只能描述个一鳞半爪吧。下面是我的理解
- 你写的程序没有 UI,即便只是命令行 也应该可以跑起来
- MVC、MVVM、MVP,我们太看重View了,让View见鬼去吧,他们没多大价值。
- 我最钦佩的一段节选:

这个给我很大的感触,假设你写的 阅读软件,你的源代码看起来像 阅读软件吗? 听起来似乎很白痴的问题,但有多少的软件是 ***Activity, ***Page, ***Controller。 比如阅读的纠错需求,你是 CorrectPage、CorrectControlelr,CorrectModel。你有建立一套业务层吗? CorrectStrategy、CorrectHighlight。
简单说你的APP,有隔离出 业务层(domain layer)吗?业务层不应该和Flutter层等代码关联。
翻译 大神文章
最后找到了, Flutter上的 TDD (Test-Drive Development)、Clean架构 相对好的,有完整代码记录的。 大神级文章 以下为翻译,同时我也加入自己的理解,尽量 信达雅吧。
TDD开发 (测试驱动开发)
涉及到库的依赖,看最后  分三个阶段
1. Write Error Test 编写错误测试
- 第一步是 根据PRD(需求文档)去编写 测试场景。
- 这时候测试场景运行一般是错误的,因为功能代码还没有开始编写
- 通常的流程是:
- 确定使用的 DataSource(如:本地、网络数据源)
- 确保 API 产生出正确的 Model
- 基于 Data 设计 UI 的 state flow (比如 无网络状态、请求失败状态、业务上的各种状态,这些状态形成流去 切换)
2. Make Test Success 让测试用例跑成功
- 开始编写功能代码
- 可以忽略整洁、新能,先使 测试情景 跑成功
3. Refactor The Code 优化、重构代码
案例学习
是一个获取天气信息的案例
作者的GitHub case地址
目录结构
lib
├── data
│ ├── constants.dart
│ ├── datasources
│ │ └── remote_data_source.dart
│ ├── exception.dart
│ ├── failure.dart
│ ├── models
│ │ └── weather_model.dart
│ └── repositories
│ └── weather_repository_impl.dart
├── domain
│ ├── entities
│ │ └── weather.dart
│ ├── repositories
│ │ └── weather_repository.dart
│ └── usecases
│ └── get_current_weather.dart
├── injection.dart
├── main.dart
└── presentation
├── bloc
│ ├── weather_bloc.dart
│ ├── weather_event.dart
│ └── weather_state.dart
└── pages
└── weather_page.dart
test
├── data
│ ├── datasources
│ │ └── remote_data_source_test.dart
│ ├── models
│ │ └── weather_model_test.dart
│ └── repositories
│ └── weather_repository_impl_test.dart
├── domain
│ └── usecases
│ └── get_current_weather_test.dart
├── helpers
│ ├── dummy_data
│ │ └── dummy_weather_response.json
│ ├── json_reader.dart
│ ├── test_helper.dart
│ └── test_helper.mocks.dart
└── presentation
├── bloc
│ └── weather_bloc_test.dart
└── pages
└── weather_page_test.dart
第一步在 domain layer 编写代码
- 因为 domain layer 使我们关注的 核心,并且他不依赖于其他的 layer
- 当然 UserCase (用例)肯定涉及到 存储,但我们会以接口的形式 调用,让 Data Layer 层去实现(这就是 依赖翻转原则, Dependency Inversion Principle)
编写测试文件
(注意,get_current_weather_test.dart 和 get_current_weather.dart 是在同一个 包名下,根目录不同而已 lib、test) 在 test/domain目录, 我们的用例是 get_current_weather_test.dart
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_weather_app_sample/domain/entities/weather.dart';
import 'package:flutter_weather_app_sample/domain/usecases/get_current_weather.dart';
import 'package:mockito/mockito.dart';
import '../../helpers/test_helper.mocks.dart';
void main() {
late MockWeatherRepository mockWeatherRepository;
late GetCurrentWeather usecase;
setUp(() {
mockWeatherRepository = MockWeatherRepository();
usecase = GetCurrentWeather(mockWeatherRepository);
});
const testWeatherDetail = Weather(
cityName: 'Jakarta',
main: 'Clouds',
description: 'few clouds',
iconCode: '02d',
temperature: 302.28,
pressure: 1009,
humidity: 70,
);
const tCityName = 'Jakarta';
test(
'should get current weather detail from the repository',
() async {
// arrange
when(mockWeatherRepository.getCurrentWeather(tCityName))
.thenAnswer((_) async => const Right(testWeatherDetail));
// act
final result = await usecase.execute(tCityName);
// assert
expect(result, equals(Right(testWeatherDetail)));
},
);
}
Domain layer 有3个部分
-
Entity
- (实体,相当于 java bean吧,由 Data Layer 里的 Module 生成。)
- 亮哥点评:老生常谈,
- 坏处:固定必须写的模板代码,由 Module转为Entity,实际上属性几乎全一样,特别是
- 好处: 1是他不依赖于 Data Layer 不用和服务端扯皮。。。。 2. 保证 Layer 的隔离,Domain Layer 不用关心 Data Layer
-
Use Cases 用例
- 用例的本质 关于如何操作 自动化系统的描述,定义输入数据,输出数据,阐明产生输出的步骤
- 这里就是获取 天气信息
- 亮哥点评:上学那会,写过好多次的 Use Case。 当然那时候 也没啥概念。
-
Repositories
- 这是一个抽象类,暴露给 Data Layer ,让他们去实现 (从本地、从网络获取)
import 'package:dartz/dartz.dart';
import 'package:flutter_weather_app_sample/data/failure.dart';
import 'package:flutter_weather_app_sample/domain/entities/weather.dart';
abstract class WeatherRepository {
Future<Either<Failure, Weather>> getCurrentWeather(String cityName);
}
我们mock一下Repository 用于测试
在test_helper.dart 中,创建 mock的 Repository
import 'package:mockito/annotations.dart';
import 'package:http/http.dart' as http;
@GenerateMocks(
[
WeatherRepository,
],
customMocks: [MockSpec<http.Client>(as: #MockHttpClient)],
)
void main() {}
执行命令,生成mock文件(这个我还没试,试后补充)
flutter pub run build_runner build
Data Layer
包含三部分
- data sources,
- models
- repositories.
models
编写测试情景,确保 model 可以转换为 entities
Ok, we will start with models, the process begins by writing testing code for the model, weather_model_test.dart. Here, we will test 3 main things:
- Is the model that we have created equal with the entities at the domain layer?
- Does the fromJson() function return a valid model?
- Does the toJson() function returns the appropriate JSON map?
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_weather_app_sample/data/models/weather_model.dart';
import 'package:flutter_weather_app_sample/domain/entities/weather.dart';
import '../../helpers/json_reader.dart';
void main() {
const tWeatherModel = WeatherModel(
cityName: 'Jakarta',
main: 'Clouds',
description: 'few clouds',
iconCode: '02d',
temperature: 302.28,
pressure: 1009,
humidity: 70,
);
const tWeather = Weather(
cityName: 'Jakarta',
main: 'Clouds',
description: 'few clouds',
iconCode: '02d',
temperature: 302.28,
pressure: 1009,
humidity: 70,
);
group('to entity', () {
test(
'should be a subclass of weather entity',
() async {
// assert
final result = tWeatherModel.toEntity();
expect(result, equals(tWeather));
},
);
});
group('from json', () {
test(
'should return a valid model from json',
() async {
// arrange
final Map<String, dynamic> jsonMap = json.decode(
readJson('helpers/dummy_data/dummy_weather_response.json'),
);
// act
final result = WeatherModel.fromJson(jsonMap);
// assert
expect(result, equals(tWeatherModel));
},
);
});
group('to json', () {
test(
'should return a json map containing proper data',
() async {
// act
final result = tWeatherModel.toJson();
// assert
final expectedJsonMap = {
'weather': [
{
'main': 'Clouds',
'description': 'few clouds',
'icon': '02d',
}
],
'main': {
'temp': 302.28,
'pressure': 1009,
'humidity': 70,
},
'name': 'Jakarta',
};
expect(result, equals(expectedJsonMap));
},
);
});
}
然后开始编写 domain 中的module,和 Entity 差不多,但多了个 JSON的互相转换。
import 'package:equatable/equatable.dart';
import 'package:flutter_weather_app_sample/domain/entities/weather.dart';
class WeatherModel extends Equatable {
const WeatherModel({
required this.cityName,
required this.main,
required this.description,
required this.iconCode,
required this.temperature,
required this.pressure,
required this.humidity,
});
final String cityName;
final String main;
final String description;
final String iconCode;
final double temperature;
final int pressure;
final int humidity;
factory WeatherModel.fromJson(Map<String, dynamic> json) => WeatherModel(
cityName: json['name'],
main: json['weather'][0]['main'],
description: json['weather'][0]['description'],
iconCode: json['weather'][0]['icon'],
temperature: json['main']['temp'],
pressure: json['main']['pressure'],
humidity: json['main']['humidity'],
);
Map<String, dynamic> toJson() => {
'weather': [
{
'main': main,
'description': description,
'icon': iconCode,
},
],
'main': {
'temp': temperature,
'pressure': pressure,
'humidity': humidity,
},
'name': cityName,
};
Weather toEntity() => Weather(
cityName: cityName,
main: main,
description: description,
iconCode: iconCode,
temperature: temperature,
pressure: pressure,
humidity: humidity,
);
@override
List<Object?> get props => [
cityName,
main,
description,
iconCode,
temperature,
pressure,
humidity,
];
}
然后编写 Repository,
未完待续 测试驱动开发—2.0
Demo的库的依赖 pubspe.yaml
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
dartz: ^0.10.1
equatable: ^2.0.3
flutter_bloc: ^8.0.1
get_it: ^7.2.0
http: ^0.13.3
rxdart: ^0.27.3
dev_dependencies:
bloc_test: ^9.0.2
build_runner: ^2.1.2
flutter_test:
sdk: flutter
mockito: ^5.0.15
mocktail: ^0.2.0
参考链接
Flutter实现Clean的一些参考 demo
- https://itnext.io/flutter-clean-architecture-b53ce9e19d5a (采用BLOC实现状态管理)
- https://medium.com/ruangguru/an-introduction-to-flutter-clean-architecture-ae00154001b0
|