Flutter应用架构模式—Clean Architecture与领域驱动设计|新宇宙博客Back to listFlutter 应用架构模式
Site Owner
Published on 2026-05-22
系统讲解Flutter分层架构设计、Clean Architecture三层依赖规则、Repository模式、UseCase及模块化策略

Flutter 应用架构模式 (App Architecture)
模块:模块十 · 架构与工程化
前置知识:状态管理(Riverpod/BLoC)、依赖注入、异步编程
为什么需要架构?
没有架构的 Flutter App 会变成这样:
// ❌ 反面教材:一个 StatefulWidget 包含所有逻辑
class ProductListPage extends StatefulWidget {
@override
State<ProductListPage> createState() => _ProductListPageState();
}
class _ProductListPageState extends State<ProductListPage> {
List<Product> products = [];
bool isLoading = false;
String? errorMessage;
// 网络请求直接在 UI 层
Future<void> fetchProducts() async {
setState(() => isLoading = true);
try {
final response = await http.get(Uri.parse('https://api.example.com/products'));
final json = jsonDecode(response.body);
// JSON 解析直接在 UI 层
setState(() {
products = (json['data'] as List).map((e) => Product.fromJson(e)).toList();
});
} catch (e) {
setState(() => errorMessage = e.toString());
} finally {
setState(() => isLoading = false);
}
}
// 业务逻辑直接在 UI 层
double get totalPrice => products.fold(0, (sum, p) => sum + p.price);
@override
Widget build(BuildContext context) {
// ...UI 代码
}
}
这种写法的问题:
- 无法单元测试(UI 和逻辑耦合)
- 无法复用业务逻辑(另一个页面也需要商品列表时要复制代码)
- 难以维护(所有改动都在一个文件)
- 数据层和展示层混杂
分层架构:三层职责划分
Clean Architecture 在 Flutter 中的落地形式:
┌─────────────────────────────────────────┐
│ Presentation Layer(展示层) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Widget │◄──│ ViewModel │ │
│ │ (UI 组件) │ │(状态持有者) │ │
│ └─────────────┘ └──────┬──────┘ │
│ │ 调用 │
├───────────────────────────────────────────┤
│ Domain Layer(领域层 / 业务逻辑层) │
│ ┌────────────┐ ┌──────────────┐ │
│ │ Use Case │ │ Entity │ │
│ │ (业务用例)│ │ (领域模型) │ │
│ └─────┬──────┘ └──────────────┘ │
│ │ 依赖抽象 │
├───────────────────────────────────────────┤
│ Data Layer(数据层) │
│ ┌────────────┐ ┌──────────────┐ │
│ │ Repository │ │ DataSource │ │
│ │ (仓库实现) │ │ (远程/本地) │ │
│ └────────────┘ └──────────────┘ │
└─────────────────────────────────────────┘
↑ 依赖方向:外层依赖内层,内层不知道外层
核心原则:依赖倒置(Dependency Inversion)
- Domain 层不依赖 Data 层,只依赖抽象(接口)
- Data 层实现 Domain 层定义的接口
- Presentation 层通过 Use Case 访问业务逻辑
目录结构设计
lib/
├── main.dart
├── app/
│ ├── app.dart # MaterialApp 入口
│ └── router.dart # go_router 路由配置
│
├── core/ # 横切关注点(跨模块共享)
│ ├── error/
│ │ ├── exceptions.dart # 应用异常类型
│ │ └── failures.dart # 失败类型(UI 可展示)
│ ├── network/
│ │ └── dio_client.dart # HTTP 客户端配置
│ └── utils/
│ └── validators.dart
│
├── features/ # 按功能模块划分
│ ├── auth/ # 认证模块
│ │ ├── data/
│ │ │ ├── datasources/
│ │ │ │ ├── auth_remote_datasource.dart
│ │ │ │ └── auth_local_datasource.dart
│ │ │ ├── models/
│ │ │ │ └── user_model.dart # DTO(包含 fromJson/toJson)
│ │ │ └── repositories/
│ │ │ └── auth_repository_impl.dart
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ │ └── user.dart # 纯 Dart 领域实体
│ │ │ ├── repositories/
│ │ │ │ └── auth_repository.dart # 抽象接口
│ │ │ └── usecases/
│ │ │ ├── login_usecase.dart
│ │ │ └── logout_usecase.dart
│ │ └── presentation/
│ │ ├── pages/
│ │ │ └── login_page.dart
│ │ ├── widgets/
│ │ │ └── login_form.dart
│ │ └── providers/ # Riverpod Notifier
│ │ └── auth_notifier.dart
│ │
│ └── products/ # 商品模块(同上结构)
│ ├── data/
│ ├── domain/
│ └── presentation/
│
└── shared/ # 跨模块共享的 UI 组件
├── widgets/
│ ├── loading_overlay.dart
│ └── error_widget.dart
└── theme/
└── app_theme.dart
Domain 层:实体与用例
领域实体(Entity)
// lib/features/products/domain/entities/product.dart
// 纯 Dart 类,没有 JSON/HTTP 依赖
// 代表业务概念,而非数据库/API 结构
class Product {
const Product({
required this.id,
required this.name,
required this.price,
required this.stockCount,
required this.category,
});
final String id;
final String name;
final double price;
final int stockCount;
final ProductCategory category;
// 领域方法:业务逻辑放在实体上
bool get isInStock => stockCount > 0;
bool get isLowStock => stockCount > 0 && stockCount <= 5;
// 价格打折(业务规则)
double discountedPrice(double discountPercent) {
assert(discountPercent >= 0 && discountPercent <= 100);
return price * (1 - discountPercent / 100);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Product && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
}
enum ProductCategory { food, electronics, clothing, books }
Repository 接口(抽象)
// lib/features/products/domain/repositories/product_repository.dart
// 只有接口,不知道数据从哪来(远程/本地/缓存)
import 'package:dartz/dartz.dart'; // Either 类型
abstract class ProductRepository {
// Either<Failure, T>:失败用 Left,成功用 Right
Future<Either<Failure, List<Product>>> getProducts({
int page = 1,
int pageSize = 20,
ProductCategory? category,
});
Future<Either<Failure, Product>> getProductById(String id);
Future<Either<Failure, void>> addToCart(String productId, int quantity);
}
Use Case(业务用例)
// lib/features/products/domain/usecases/get_products_usecase.dart
// 每个 Use Case 只做一件事,遵循单一职责原则
class GetProductsUseCase {
const GetProductsUseCase(this._repository);
final ProductRepository _repository;
// 调用形式:单一方法 call(),允许像函数一样调用
Future<Either<Failure, List<Product>>> call(GetProductsParams params) {
return _repository.getProducts(
page: params.page,
pageSize: params.pageSize,
category: params.category,
);
}
}
class GetProductsParams {
const GetProductsParams({
this.page = 1,
this.pageSize = 20,
this.category,
});
final int page;
final int pageSize;
final ProductCategory? category;
}
Data 层:Repository 实现
数据模型(Model = Entity + JSON 序列化)
// lib/features/products/data/models/product_model.dart
// Model 继承 Entity,添加序列化能力
import 'package:freezed_annotation/freezed_annotation.dart';
part 'product_model.freezed.dart';
part 'product_model.g.dart';
@freezed
class ProductModel with _$ProductModel {
const factory ProductModel({
required String id,
required String name,
required double price,
@JsonKey(name: 'stock_count') required int stockCount,
required String category,
}) = _ProductModel;
factory ProductModel.fromJson(Map<String, dynamic> json) =>
_$ProductModelFromJson(json);
}
// Model → Entity 转换(隔离层)
extension ProductModelMapper on ProductModel {
Product toEntity() => Product(
id: id,
name: name,
price: price,
stockCount: stockCount,
category: _parseCategory(category),
);
static ProductCategory _parseCategory(String category) {
return switch (category) {
'food' => ProductCategory.food,
'electronics' => ProductCategory.electronics,
'clothing' => ProductCategory.clothing,
_ => ProductCategory.books,
};
}
}
Repository 实现
// lib/features/products/data/repositories/product_repository_impl.dart
class ProductRepositoryImpl implements ProductRepository {
const ProductRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
final ProductRemoteDataSource remoteDataSource;
final ProductLocalDataSource localDataSource;
final NetworkInfo networkInfo;
@override
Future<Either<Failure, List<Product>>> getProducts({
int page = 1,
int pageSize = 20,
ProductCategory? category,
}) async {
if (await networkInfo.isConnected) {
try {
// 有网络:从远程获取并缓存
final models = await remoteDataSource.getProducts(
page: page,
pageSize: pageSize,
);
await localDataSource.cacheProducts(models); // 写入本地缓存
return Right(models.map((m) => m.toEntity()).toList());
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message));
}
} else {
try {
// 无网络:读取本地缓存(离线优先)
final cached = await localDataSource.getCachedProducts();
return Right(cached.map((m) => m.toEntity()).toList());
} on CacheException {
return const Left(CacheFailure(message: '无网络连接且没有缓存数据'));
}
}
}
// ...其他方法
}
Presentation 层:Riverpod Notifier
// lib/features/products/presentation/providers/product_notifier.dart
// 使用 Riverpod 的 AsyncNotifier(推荐 Flutter 3.x 以后的写法)
@riverpod
class ProductNotifier extends _$ProductNotifier {
@override
Future<List<Product>> build() async {
// Notifier 构建时自动加载
return _fetchProducts();
}
Future<List<Product>> _fetchProducts({int page = 1}) async {
final useCase = ref.read(getProductsUseCaseProvider);
final result = await useCase(GetProductsParams(page: page));
return result.fold(
(failure) => throw ProductException(failure.message), // 失败抛出异常
(products) => products, // 成功返回数据
);
}
// 刷新
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(_fetchProducts);
}
// 加载更多
Future<void> loadMore() async {
final currentProducts = state.valueOrNull ?? [];
final nextPage = (currentProducts.length ~/ 20) + 1;
state = const AsyncLoading<List<Product>>().copyWithPrevious(state);
try {
final newProducts = await _fetchProducts(page: nextPage);
state = AsyncData([...currentProducts, ...newProducts]);
} catch (e, st) {
state = AsyncError(e, st);
}
}
}
// lib/features/products/presentation/pages/product_list_page.dart
class ProductListPage extends ConsumerWidget {
const ProductListPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productNotifierProvider);
return Scaffold(
appBar: AppBar(title: const Text('商品列表')),
body: productsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => _ErrorView(
message: error.toString(),
onRetry: () => ref.invalidate(productNotifierProvider),
),
data: (products) => _ProductListView(products: products),
),
);
}
}
依赖注入:get_it + injectable
// lib/core/injection/injection.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
final GetIt getIt = GetIt.instance;
@InjectableInit(
initializerName: 'init',
preferRelativeImports: true,
asExtension: false,
)
void configureDependencies() => init(getIt);
// 在各层使用注解标记依赖
// lib/features/products/data/repositories/product_repository_impl.dart
@LazySingleton(as: ProductRepository) // 注册为接口的实现
class ProductRepositoryImpl implements ProductRepository {
const ProductRepositoryImpl(
this.remoteDataSource, // 自动注入
this.localDataSource,
this.networkInfo,
);
// ...
}
// lib/features/products/domain/usecases/get_products_usecase.dart
@lazySingleton
class GetProductsUseCase {
const GetProductsUseCase(this._repository);
final ProductRepository _repository; // 注入接口,不依赖具体实现
// ...
}
// main.dart
void main() {
configureDependencies(); // 初始化依赖注入容器
runApp(const ProviderScope(child: MyApp())); // Riverpod 作用域
}
// Riverpod Provider 桥接 get_it
@riverpod
GetProductsUseCase getProductsUseCase(GetProductsUseCaseRef ref) {
return getIt<GetProductsUseCase>();
}
错误处理统一策略
// lib/core/error/failures.dart
// 失败类型(UI 层可以展示的错误)
sealed class Failure {
const Failure({required this.message});
final String message;
}
class ServerFailure extends Failure {
const ServerFailure({required super.message, this.statusCode});
final int? statusCode;
}
class NetworkFailure extends Failure {
const NetworkFailure() : super(message: '网络连接失败,请检查网络设置');
}
class CacheFailure extends Failure {
const CacheFailure({required super.message});
}
class AuthFailure extends Failure {
const AuthFailure({required super.message});
}
// UI 层统一处理 Failure
extension FailureExtension on Failure {
String get userMessage => switch (this) {
ServerFailure(:final statusCode) when statusCode == 401 => '登录已过期,请重新登录',
ServerFailure(:final statusCode) when statusCode == 404 => '请求的内容不存在',
ServerFailure(:final message) => '服务器错误:$message',
NetworkFailure() => '网络连接失败,请检查网络',
CacheFailure() => '本地数据读取失败',
AuthFailure(:final message) => message,
};
}
架构决策:什么规模用什么方案
| 项目规模 | 推荐方案 | 理由 |
|---|
| 个人项目 / Demo | setState + Riverpod | 快速上手,够用 |
| 小型 App(< 5人) | Riverpod + Repository | 结构清晰,不过度设计 |
| 中型 App(5-15人) | Clean Architecture + Riverpod | 可测试、可维护 |
| 大型 App(> 15人) | Clean Architecture + BLoC + Modular | 团队协作、代码隔离 |
过关标准
✅ 能解释每一层的依赖方向(内层不依赖外层)
✅ 能说明 Repository 模式如何实现数据源的无缝切换
✅ 能用 Clean Architecture 重构一个中型 App(至少包含网络请求、本地缓存、状态管理三层)
✅ 理解 Use Case 为什么只有 call() 方法
✅ 能配置 get_it + injectable 实现自动依赖注入