🧱 第一章:为什么需要架构
1.1 没有架构会怎样?
在小型项目中,把所有逻辑写在 Widget 里似乎可以正常工作。但随着项目规模增长,问题很快暴露出来:
代码耦合严重
UI 逻辑、业务逻辑、数据获取混杂在同一个 Widget 中,修改一个功能可能影响其他功能,牵一发而动全身。
难以测试
当业务逻辑嵌入 Widget 时,必须启动整个 UI 才能测试一段计算逻辑,测试成本极高。
团队协作困难
多人同时修改同一个文件导致频繁冲突,没有明确的职责划分,代码审查也变得困难。
难以复用
逻辑与特定 UI 绑定,无法在不同页面或项目中复用业务逻辑和数据处理代码。
1.2 关注点分离
关注点分离(Separation of Concerns)是架构设计的核心原则。每个模块只负责一件事情:
- 表现层(Presentation):负责 UI 渲染和用户交互
- 业务逻辑层(Domain):负责核心业务规则,不依赖任何框架
- 数据层(Data):负责数据获取、缓存和持久化
1.3 可测试性
良好的架构让每一层都可以独立测试。业务逻辑不依赖 Flutter 框架,可以用纯 Dart 单元测试快速验证;UI 层只负责展示,可以用 Widget 测试验证界面行为。
反例:所有逻辑堆在 Widget 中 // 这段代码将网络请求、状态管理、UI 全部耦合在一起 class UserPage extends StatefulWidget { @override State<UserPage> createState() => _UserPageState(); } class _UserPageState extends State<UserPage> { List<User> users = []; bool isLoading = false; @override void initState() { super.initState(); _loadUsers(); // 网络请求直接写在 Widget 中 } Future<void> _loadUsers() async { setState(() => isLoading = true); // 直接发起 HTTP 请求 —— 无法独立测试 final response = await http.get(Uri.parse('https://api.example.com/users')); setState(() { users = jsonDecode(response.body); isLoading = false; }); } @override Widget build(BuildContext context) { /* ... */ } }
📐 第二章:MVVM 模式
2.1 MVVM 概念
MVVM(Model-View-ViewModel)是 Flutter 中最常用的架构模式之一。它将应用分为三个部分:
Model
数据模型,表示应用的核心数据结构。纯 Dart 类,不依赖任何框架。
View
UI 层,即 Flutter Widget。只负责展示数据和接收用户输入,不包含业务逻辑。
ViewModel
连接 Model 和 View 的桥梁。持有状态、处理业务逻辑,通过通知机制更新 View。
2.2 使用 ChangeNotifier 实现 ViewModel
Flutter 原生的 ChangeNotifier 配合 Provider 是实现 MVVM 最简单的方式:
Model - user.dart class User { final String id; final String name; final String email; const User({ required this.id, required this.name, required this.email, }); factory User.fromJson(Map<String, dynamic> json) { return User( id: json['id'], name: json['name'], email: json['email'], ); } }
ViewModel - user_view_model.dart import 'package:flutter/foundation.dart'; class UserViewModel extends ChangeNotifier { final UserRepository _repository; List<User> _users = []; List<User> get users => _users; bool _isLoading = false; bool get isLoading => _isLoading; String? _errorMessage; String? get errorMessage => _errorMessage; UserViewModel(this._repository); /// 加载用户列表 Future<void> loadUsers() async { _isLoading = true; _errorMessage = null; notifyListeners(); try { _users = await _repository.getUsers(); } catch (e) { _errorMessage = '加载失败:$e'; } finally { _isLoading = false; notifyListeners(); } } /// 删除用户 Future<void> deleteUser(String id) async { await _repository.deleteUser(id); _users.removeWhere((u) => u.id == id); notifyListeners(); } }
View - user_page.dart class UserPage extends StatelessWidget { const UserPage({super.key}); @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => UserViewModel(getIt<UserRepository>())..loadUsers(), child: Scaffold( appBar: AppBar(title: const Text('用户列表')), body: Consumer<UserViewModel>( builder: (context, vm, child) { if (vm.isLoading) { return const Center(child: CircularProgressIndicator()); } if (vm.errorMessage != null) { return Center(child: Text(vm.errorMessage!)); } return ListView.builder( itemCount: vm.users.length, itemBuilder: (context, index) { final user = vm.users[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), trailing: IconButton( icon: const Icon(Icons.delete), onPressed: () => vm.deleteUser(user.id), ), ); }, ); }, ), ), ); } }
BuildContext 或 Widget 引用。它是纯 Dart 类(继承 ChangeNotifier),可以被完全独立地单元测试。
🏛️ 第三章:Clean Architecture
3.1 三层架构
Clean Architecture 由 Robert C. Martin 提出,其核心思想是让业务逻辑独立于框架、UI 和数据源。在 Flutter 中通常分为三层:
Presentation 层
Widget、Page、ViewModel/Bloc。负责 UI 渲染,依赖 Domain 层获取数据。
Domain 层
Entity、UseCase、Repository 接口。纯 Dart 代码,不依赖任何外部库。
Data 层
Repository 实现、DataSource、Model(DTO)。负责与 API、数据库等外部系统交互。
3.2 依赖规则
依赖方向只能从外向内:Presentation → Domain ← Data。Domain 层是最内层,不依赖任何其他层。Data 层通过实现 Domain 层定义的 Repository 接口来提供数据。
3.3 目录结构
项目目录结构 lib/ ├── core/ # 公共工具 │ ├── error/ # 异常和失败类 │ │ ├── exceptions.dart │ │ └── failures.dart │ ├── network/ # 网络相关工具 │ │ └── network_info.dart │ └── usecases/ # UseCase 基类 │ └── usecase.dart ├── features/ # 功能模块 │ └── user/ │ ├── data/ # 数据层 │ │ ├── datasources/ │ │ │ ├── user_remote_data_source.dart │ │ │ └── user_local_data_source.dart │ │ ├── models/ │ │ │ └── user_model.dart │ │ └── repositories/ │ │ └── user_repository_impl.dart │ ├── domain/ # 领域层 │ │ ├── entities/ │ │ │ └── user.dart │ │ ├── repositories/ │ │ │ └── user_repository.dart │ │ └── usecases/ │ │ ├── get_users.dart │ │ └── delete_user.dart │ └── presentation/ # 表现层 │ ├── viewmodels/ │ │ └── user_viewmodel.dart │ ├── pages/ │ │ └── user_page.dart │ └── widgets/ │ └── user_card.dart └── injection_container.dart # 依赖注入配置
3.4 Domain 层代码
domain/entities/user.dart // Entity:纯业务数据,不包含序列化逻辑 class User { final String id; final String name; final String email; const User({ required this.id, required this.name, required this.email, }); }
domain/repositories/user_repository.dart import 'package:dartz/dartz.dart'; // Repository 接口:定义数据操作契约 abstract class UserRepository { Future<Either<Failure, List<User>>> getUsers(); Future<Either<Failure, User>> getUserById(String id); Future<Either<Failure, void>> deleteUser(String id); }
core/usecases/usecase.dart import 'package:dartz/dartz.dart'; // UseCase 基类 abstract class UseCase<Type, Params> { Future<Either<Failure, Type>> call(Params params); } // 无参数时使用 class NoParams { const NoParams(); }
domain/usecases/get_users.dart class GetUsers extends UseCase<List<User>, NoParams> { final UserRepository repository; GetUsers(this.repository); @override Future<Either<Failure, List<User>>> call(NoParams params) { return repository.getUsers(); } }
3.5 Data 层代码
data/models/user_model.dart // Model(DTO):继承 Entity,添加序列化逻辑 class UserModel extends User { const UserModel({ required super.id, required super.name, required super.email, }); factory UserModel.fromJson(Map<String, dynamic> json) { return UserModel( id: json['id'] as String, name: json['name'] as String, email: json['email'] as String, ); } Map<String, dynamic> toJson() { return {'id': id, 'name': name, 'email': email}; } }
data/datasources/user_remote_data_source.dart abstract class UserRemoteDataSource { Future<List<UserModel>> getUsers(); Future<void> deleteUser(String id); } class UserRemoteDataSourceImpl implements UserRemoteDataSource { final Dio dio; UserRemoteDataSourceImpl({required this.dio}); @override Future<List<UserModel>> getUsers() async { final response = await dio.get('/users'); return (response.data as List) .map((json) => UserModel.fromJson(json)) .toList(); } @override Future<void> deleteUser(String id) async { await dio.delete('/users/$id'); } }
data/repositories/user_repository_impl.dart class UserRepositoryImpl implements UserRepository { final UserRemoteDataSource remoteDataSource; final UserLocalDataSource localDataSource; final NetworkInfo networkInfo; UserRepositoryImpl({ required this.remoteDataSource, required this.localDataSource, required this.networkInfo, }); @override Future<Either<Failure, List<User>>> getUsers() async { if (await networkInfo.isConnected) { try { final users = await remoteDataSource.getUsers(); await localDataSource.cacheUsers(users); return Right(users); } on ServerException { return Left(ServerFailure()); } } else { try { final cachedUsers = await localDataSource.getCachedUsers(); return Right(cachedUsers); } on CacheException { return Left(CacheFailure()); } } } @override Future<Either<Failure, void>> deleteUser(String id) async { try { await remoteDataSource.deleteUser(id); return const Right(null); } on ServerException { return Left(ServerFailure()); } } }
dartz 包的 Either 类型来处理成功和失败两种情况,避免在业务逻辑中使用 try-catch。Left 表示失败,Right 表示成功。
🗂️ 第四章:Repository Pattern
4.1 抽象 Repository
Repository 模式的核心是定义抽象接口,将数据访问细节与业务逻辑隔离。上层代码只依赖接口,不关心数据来自网络还是本地缓存。
抽象 Repository 接口 abstract class ArticleRepository { /// 获取文章列表,支持分页 Future<Either<Failure, List<Article>>> getArticles({int page = 1}); /// 获取文章详情 Future<Either<Failure, Article>> getArticleById(String id); /// 收藏文章 Future<Either<Failure, void>> favoriteArticle(String id); /// 获取本地收藏列表 Future<Either<Failure, List<Article>>> getFavorites(); }
4.2 Remote 和 Local DataSource
远程数据源 abstract class ArticleRemoteDataSource { Future<List<ArticleModel>> getArticles(int page); Future<ArticleModel> getArticleById(String id); Future<void> favoriteArticle(String id); } class ArticleRemoteDataSourceImpl implements ArticleRemoteDataSource { final Dio dio; ArticleRemoteDataSourceImpl({required this.dio}); @override Future<List<ArticleModel>> getArticles(int page) async { final response = await dio.get('/articles', queryParameters: {'page': page}); return (response.data['data'] as List) .map((e) => ArticleModel.fromJson(e)) .toList(); } @override Future<ArticleModel> getArticleById(String id) async { final response = await dio.get('/articles/$id'); return ArticleModel.fromJson(response.data); } @override Future<void> favoriteArticle(String id) async { await dio.post('/articles/$id/favorite'); } }
本地数据源 - 使用 Hive 缓存 abstract class ArticleLocalDataSource { Future<List<ArticleModel>> getCachedArticles(); Future<void> cacheArticles(List<ArticleModel> articles); Future<List<ArticleModel>> getFavorites(); Future<void> saveFavorite(ArticleModel article); } class ArticleLocalDataSourceImpl implements ArticleLocalDataSource { final Box<ArticleModel> articlesBox; final Box<ArticleModel> favoritesBox; ArticleLocalDataSourceImpl({ required this.articlesBox, required this.favoritesBox, }); @override Future<List<ArticleModel>> getCachedArticles() async { if (articlesBox.isEmpty) throw CacheException(); return articlesBox.values.toList(); } @override Future<void> cacheArticles(List<ArticleModel> articles) async { await articlesBox.clear(); for (final article in articles) { await articlesBox.put(article.id, article); } } @override Future<List<ArticleModel>> getFavorites() async { return favoritesBox.values.toList(); } @override Future<void> saveFavorite(ArticleModel article) async { await favoritesBox.put(article.id, article); } }
4.3 带缓存策略的 Repository 实现
repository 实现 - 离线优先策略 class ArticleRepositoryImpl implements ArticleRepository { final ArticleRemoteDataSource remote; final ArticleLocalDataSource local; final NetworkInfo networkInfo; ArticleRepositoryImpl({ required this.remote, required this.local, required this.networkInfo, }); @override Future<Either<Failure, List<Article>>> getArticles({int page = 1}) async { if (await networkInfo.isConnected) { try { final articles = await remote.getArticles(page); if (page == 1) { await local.cacheArticles(articles); // 仅缓存第一页 } return Right(articles); } on ServerException catch (e) { return Left(ServerFailure(message: e.message)); } } else { try { final cached = await local.getCachedArticles(); return Right(cached); } on CacheException { return Left(CacheFailure(message: '无网络且无缓存数据')); } } } }
4.4 错误处理
统一错误处理体系 // 异常类 —— 由 DataSource 抛出 class ServerException implements Exception { final String message; final int? statusCode; ServerException({this.message = '服务器异常', this.statusCode}); } class CacheException implements Exception { final String message; CacheException({this.message = '缓存异常'}); } // 失败类 —— 由 Repository 返回,供上层使用 abstract class Failure { final String message; const Failure({this.message = '未知错误'}); } class ServerFailure extends Failure { const ServerFailure({super.message = '服务器错误'}); } class CacheFailure extends Failure { const CacheFailure({super.message = '缓存错误'}); } class NetworkFailure extends Failure { const NetworkFailure({super.message = '网络连接失败'}); }
💉 第五章:依赖注入
5.1 get_it 服务定位器
get_it 是 Flutter 中最常用的依赖注入库,它是一个简单的服务定位器(Service Locator),支持单例、懒加载单例和工厂模式。
pubspec.yaml dependencies: get_it: ^7.6.0 injectable: ^2.3.0 dev_dependencies: injectable_generator: ^2.4.0 build_runner: ^2.4.0
5.2 注册类型
injection_container.dart import 'package:get_it/get_it.dart'; final getIt = GetIt.instance; Future<void> configureDependencies() async { // ============ 外部依赖 ============ final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com')); getIt.registerSingleton<Dio>(dio); // ============ 核心服务 ============ // registerSingleton:立即创建,全局唯一 getIt.registerSingleton<NetworkInfo>( NetworkInfoImpl(), ); // registerLazySingleton:首次使用时创建,全局唯一 getIt.registerLazySingleton<UserRemoteDataSource>( () => UserRemoteDataSourceImpl(dio: getIt()), ); getIt.registerLazySingleton<UserLocalDataSource>( () => UserLocalDataSourceImpl(), ); // Repository getIt.registerLazySingleton<UserRepository>( () => UserRepositoryImpl( remoteDataSource: getIt(), localDataSource: getIt(), networkInfo: getIt(), ), ); // UseCases getIt.registerLazySingleton(() => GetUsers(getIt())); // registerFactory:每次请求创建新实例 getIt.registerFactory( () => UserViewModel(repository: getIt()), ); }
5.3 在 main 中初始化
main.dart void main() async { WidgetsFlutterBinding.ensureInitialized(); await configureDependencies(); runApp(const MyApp()); } // 在任意位置使用 final userRepo = getIt<UserRepository>(); final vm = getIt<UserViewModel>();
5.4 使用 injectable 自动生成
手动注册依赖容易遗漏和出错。injectable 通过注解自动生成注册代码:
使用注解标记类 import 'package:injectable/injectable.dart'; @singleton // 单例 class NetworkInfoImpl implements NetworkInfo { // ... } @lazySingleton // 懒加载单例 class UserRemoteDataSourceImpl implements UserRemoteDataSource { final Dio dio; UserRemoteDataSourceImpl(this.dio); } @injectable // 工厂模式,每次创建新实例 class UserViewModel extends ChangeNotifier { final UserRepository repository; UserViewModel(this.repository); } // 运行代码生成 // dart run build_runner build --delete-conflicting-outputs
registerSingleton 在注册时立即创建实例,适合必须提前初始化的服务;registerLazySingleton 延迟到首次使用时创建,适合大部分场景;registerFactory 每次获取都创建新实例,适合 ViewModel 等需要独立状态的对象。
🧪 第六章:单元测试
6.1 test 包基础
Flutter 的 test 包提供了编写单元测试的核心 API。单元测试用于验证单个函数、方法或类的行为是否正确。
pubspec.yaml dev_dependencies: test: ^1.24.0 mocktail: ^1.0.0 # 或者 mockito + build_runner
test/unit/calculator_test.dart import 'package:test/test.dart'; import 'package:my_app/calculator.dart'; void main() { // group 将相关测试分组 group('Calculator', () { late Calculator calculator; // setUp 在每个测试前运行 setUp(() { calculator = Calculator(); }); test('两个正数相加', () { expect(calculator.add(2, 3), equals(5)); }); test('除以零抛出异常', () { expect( () => calculator.divide(10, 0), throwsA(isA<ArgumentError>()), ); }); test('浮点数近似匹配', () { expect(calculator.divide(10, 3), closeTo(3.333, 0.01)); }); }); }
6.2 常用 Matchers
常用断言匹配器 // 相等 expect(value, equals(42)); expect(value, isNot(equals(0))); // 类型检查 expect(value, isA<String>()); expect(value, isNull); expect(value, isNotNull); // 集合 expect(list, contains('hello')); expect(list, hasLength(3)); expect(list, isEmpty); expect(list, containsAll(['a', 'b'])); // 比较 expect(value, greaterThan(10)); expect(value, lessThanOrEqualTo(100)); expect(value, inInclusiveRange(1, 10)); // 字符串 expect(str, startsWith('hello')); expect(str, endsWith('world')); expect(str, matches(RegExp(r'\d+'))); // 异常 expect(() => doSomething(), throwsException); expect(() => doSomething(), throwsA(isA<FormatException>()));
6.3 使用 mocktail 模拟依赖
test/unit/user_viewmodel_test.dart import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; // 创建 Mock 类 class MockUserRepository extends Mock implements UserRepository {} void main() { late MockUserRepository mockRepo; late UserViewModel viewModel; setUp(() { mockRepo = MockUserRepository(); viewModel = UserViewModel(mockRepo); }); final testUsers = [ const User(id: '1', name: '张三', email: 'zhang@test.com'), const User(id: '2', name: '李四', email: 'li@test.com'), ]; group('loadUsers', () { test('成功加载用户列表', () async { // Arrange - 设置 Mock 行为 when(() => mockRepo.getUsers()) .thenAnswer((_) async => Right(testUsers)); // Act - 执行操作 await viewModel.loadUsers(); // Assert - 验证结果 expect(viewModel.users, equals(testUsers)); expect(viewModel.isLoading, isFalse); expect(viewModel.errorMessage, isNull); // 验证方法被调用了一次 verify(() => mockRepo.getUsers()).called(1); }); test('加载失败设置错误信息', () async { when(() => mockRepo.getUsers()) .thenAnswer((_) async => Left(ServerFailure())); await viewModel.loadUsers(); expect(viewModel.users, isEmpty); expect(viewModel.errorMessage, isNotNull); }); }); group('deleteUser', () { test('删除后从列表中移除', () async { // 先加载数据 when(() => mockRepo.getUsers()) .thenAnswer((_) async => Right(testUsers)); when(() => mockRepo.deleteUser('1')) .thenAnswer((_) async => const Right(null)); await viewModel.loadUsers(); await viewModel.deleteUser('1'); expect(viewModel.users.length, equals(1)); expect(viewModel.users.first.name, equals('李四')); }); }); }
6.4 异步测试
异步测试技巧 test('异步操作测试', () async { // 使用 async/await final result = await service.fetchData(); expect(result, isNotNull); }); test('Stream 测试', () { // 使用 emitsInOrder 验证 Stream 输出顺序 expect( counterStream, emitsInOrder([1, 2, 3]), ); }); test('超时测试', () async { // 设置超时时间 await service.slowOperation(); }, timeout: Timeout(const Duration(seconds: 10))); test('使用 fake async 控制时间', () { fakeAsync((async) { var completed = false; Future.delayed(const Duration(seconds: 5), () { completed = true; }); // 快进 5 秒 async.elapse(const Duration(seconds: 5)); expect(completed, isTrue); }); });
flutter test 运行所有测试,flutter test test/unit/ 运行特定目录,flutter test --coverage 生成覆盖率报告。
🖼️ 第七章:Widget 测试
7.1 testWidgets 基础
Widget 测试在模拟环境中渲染 Widget,不需要真实设备。它比单元测试多了 UI 渲染能力,比集成测试快得多。
test/widget/counter_page_test.dart import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:my_app/counter_page.dart'; void main() { testWidgets('计数器初始值为 0', (WidgetTester tester) async { // 构建 Widget await tester.pumpWidget( const MaterialApp(home: CounterPage()), ); // 查找并验证 expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); }); testWidgets('点击加号按钮计数器加一', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(home: CounterPage()), ); // 点击加号按钮 await tester.tap(find.byIcon(Icons.add)); // 触发重建 await tester.pump(); // 验证结果 expect(find.text('1'), findsOneWidget); expect(find.text('0'), findsNothing); }); }
7.2 Finder 查找方式
常用 Finder // 按文本查找 find.text('Hello'); // 按 Widget 类型查找 find.byType(ElevatedButton); // 按 Icon 查找 find.byIcon(Icons.add); // 按 Key 查找 find.byKey(const Key('submit_button')); // 按 Widget 实例查找 find.byWidget(myWidget); // 后代查找 —— 在特定 Widget 下查找 find.descendant( of: find.byType(AppBar), matching: find.text('标题'), ); // 匹配数量断言 expect(finder, findsOneWidget); // 恰好一个 expect(finder, findsNothing); // 零个 expect(finder, findsNWidgets(3)); // 恰好 N 个 expect(finder, findsAtLeastNWidgets(1)); // 至少 N 个
7.3 用户交互操作
WidgetTester 交互方法 // 点击 await tester.tap(find.byType(ElevatedButton)); // 输入文本 await tester.enterText(find.byType(TextField), 'Hello World'); // 长按 await tester.longPress(find.byType(ListTile)); // 拖拽 await tester.drag(find.byType(ListView), const Offset(0, -300)); // 重建 Widget(无动画) await tester.pump(); // 等待所有动画完成 await tester.pumpAndSettle(); // 等待异步操作 await tester.pump(const Duration(seconds: 1));
7.4 完整 Widget 测试示例
test/widget/login_page_test.dart void main() { testWidgets('登录页面完整交互测试', (WidgetTester tester) async { // 构建登录页面 await tester.pumpWidget( MaterialApp(home: LoginPage()), ); // 验证初始状态:登录按钮存在但禁用 final loginButton = find.byKey(const Key('login_button')); expect(loginButton, findsOneWidget); // 输入用户名 await tester.enterText( find.byKey(const Key('email_field')), 'user@example.com', ); // 输入密码 await tester.enterText( find.byKey(const Key('password_field')), 'password123', ); await tester.pump(); // 点击登录按钮 await tester.tap(loginButton); await tester.pumpAndSettle(); // 验证显示加载指示器 expect(find.byType(CircularProgressIndicator), findsOneWidget); // 等待异步操作完成 await tester.pumpAndSettle(); // 验证登录成功后导航到首页 expect(find.byType(HomePage), findsOneWidget); }); testWidgets('输入无效邮箱显示错误信息', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp(home: LoginPage()), ); // 输入无效邮箱 await tester.enterText( find.byKey(const Key('email_field')), 'invalid-email', ); // 点击登录 await tester.tap(find.byKey(const Key('login_button'))); await tester.pump(); // 验证显示错误提示 expect(find.text('请输入有效的邮箱地址'), findsOneWidget); }); }
pump() 只触发一帧的重建,适合立即生效的状态变化;pumpAndSettle() 等待所有动画和异步帧完成,适合有动画的场景。对于网络请求等真正的异步操作,需要配合 Mock 使用。
📱 第八章:集成测试
8.1 integration_test 包
集成测试在真实设备或模拟器上运行,测试完整的应用流程。它能验证多个模块协同工作的表现。
pubspec.yaml
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
8.2 编写集成测试
integration_test/app_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:my_app/main.dart' as app; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('端到端测试', () { testWidgets('完整登录流程', (WidgetTester tester) async { // 启动完整应用 app.main(); await tester.pumpAndSettle(); // 验证在登录页面 expect(find.text('登录'), findsOneWidget); // 输入凭证 await tester.enterText( find.byKey(const Key('email_field')), 'test@example.com', ); await tester.enterText( find.byKey(const Key('password_field')), 'password123', ); // 点击登录 await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); // 验证登录成功,进入首页 expect(find.text('首页'), findsOneWidget); // 点击用户列表 await tester.tap(find.text('用户管理')); await tester.pumpAndSettle(); // 验证用户列表加载 expect(find.byType(ListView), findsOneWidget); }); }); }
8.3 在设备上运行集成测试
运行集成测试 # 在连接的设备上运行 flutter test integration_test/app_test.dart # 在指定设备上运行 flutter test integration_test/app_test.dart -d <device_id> # 在 Chrome 上运行(Web) flutter test integration_test/app_test.dart -d chrome # 运行所有集成测试 flutter test integration_test/ # 截图(用于 CI 报告) # 在测试代码中: # await binding.takeScreenshot('login_page');
📏 第九章:代码规范
9.1 analysis_options.yaml
Dart 的静态分析工具通过 analysis_options.yaml 配置,它定义了代码规范规则,帮助团队保持一致的代码风格。
analysis_options.yaml - 基础配置 # 使用 Flutter 官方推荐规则 include: package:flutter_lints/flutter.yaml analyzer: errors: # 将某些警告提升为错误 missing_return: error dead_code: warning unused_import: warning exclude: - "**/*.g.dart" # 排除生成的代码 - "**/*.freezed.dart" linter: rules: # 代码风格 - prefer_const_constructors - prefer_const_declarations - prefer_final_locals - prefer_single_quotes - sort_constructors_first - sort_unnamed_constructors_first # 类型安全 - always_declare_return_types - avoid_dynamic_calls - prefer_typing_uninitialized_variables # 可读性 - avoid_print # 禁止使用 print,使用 log - require_trailing_commas
9.2 flutter_lints 与 very_good_analysis
pubspec.yaml # 方案一:Flutter 官方规则(默认) dev_dependencies: flutter_lints: ^3.0.0 # 方案二:Very Good Ventures 严格规则(推荐) dev_dependencies: very_good_analysis: ^5.1.0
使用 very_good_analysis # analysis_options.yaml include: package:very_good_analysis/analysis_options.yaml # 可以覆盖某些规则 linter: rules: public_member_api_docs: false # 关闭强制文档注释
9.3 运行分析和格式化
命令行工具 # 静态分析 flutter analyze dart analyze # 代码格式化 dart format . dart format --set-exit-if-changed . # CI 中使用,有未格式化代码则失败 # 自动修复 lint 问题 dart fix --apply # 查看可修复的问题 dart fix --dry-run
dart format --set-exit-if-changed . 和 flutter analyze --fatal-infos,确保所有提交的代码都符合团队规范。建议使用 very_good_analysis 作为基础规则,它比默认的 flutter_lints 更严格,有助于提升代码质量。
✏️ 第十章:实践练习
练习 1:Clean Architecture 笔记应用
使用 Clean Architecture 搭建一个笔记管理应用,练习完整的三层架构设计。
- 按照
features/note/目录结构组织代码,包含 data、domain、presentation 三层 - 定义
NoteEntity 和NoteModel(带 JSON 序列化) - 创建
NoteRepository抽象接口,实现增删改查四个方法 - 使用 Hive 作为本地数据源,实现
NoteLocalDataSource - 使用
get_it注册所有依赖 - 编写 ViewModel 并通过 Provider 连接 UI
练习 2:为 ViewModel 编写完整单元测试
为练习 1 中的 NoteViewModel 编写全覆盖的单元测试。
- 使用
mocktail创建MockNoteRepository - 测试
loadNotes:成功加载、加载失败、空列表三种情况 - 测试
addNote:成功添加后列表更新 - 测试
deleteNote:删除后从列表移除 - 验证每个操作中
isLoading状态的变化(先变 true 再变 false) - 确保测试覆盖率达到 90% 以上
练习 3:Widget 测试与集成测试
为笔记应用的核心页面编写 Widget 测试和一个端到端集成测试。
- 编写 Widget 测试:验证笔记列表页正确渲染、空状态显示提示文字
- 编写 Widget 测试:验证新建笔记对话框的输入和提交功能
- 编写 Widget 测试:验证滑动删除笔记的交互
- 编写一个集成测试:从打开应用到新建笔记到查看笔记详情的完整流程
- 配置
analysis_options.yaml使用very_good_analysis规则 - 确保
flutter analyze和dart format全部通过