Flutter项目架构设计:从业务分层到代码复用
Flutter项目架构设计:从业务分层到代码复用
Flutter 项目做到一定规模后,代码组织方式会直接影响开发效率和后期维护成本。之前接手过一个项目,页面组件直接调接口,状态和 UI 全部耦合在一起,加一个字段要改七八个文件。
整理一下 Flutter 项目的分层架构设计思路,以及怎么在实际项目中落地。
常见架构模式
Flutter 生态里常见的架构模式有几种:
MVC:最原始的分层
Model-View-Controller,UI 层负责展示,Controller 负责处理输入,Model 负责数据。
Flutter 的 StatefulWidget 其实天然就是 MVC:Widget 是 View,State 是 Controller,数据可以放在 State 里也可以单独抽成 Model。但实际项目里 MVC 容易变成 Mass View Controller,所有逻辑都堆在 State 里。
MVP:View 和 Presenter 分离
View 负责 UI 展示,Presenter 负责业务逻辑,Model 只负责数据结构。View 和 Presenter 通过接口通信。
Flutter 实现 MVP:
// View 接口
abstract class LoginView {
void showLoading();
void hideLoading();
void showError(String message);
void navigateToHome();
}
// Presenter
class LoginPresenter {
final LoginView view;
final AuthService authService;
LoginPresenter(this.view, this.authService);
Future<void> login(String phone, String code) async {
view.showLoading();
try {
await authService.login(phone, code);
view.hideLoading();
view.navigateToHome();
} catch (e) {
view.hideLoading();
view.showError(e.toString());
}
}
}
// Widget 实现 View
class LoginPage extends StatefulWidget {
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> implements LoginView {
late LoginPresenter _presenter;
bool _isLoading = false;
void initState() {
super.initState();
_presenter = LoginPresenter(this, AuthService());
}
Widget build(BuildContext context) {
return Scaffold(
body: _isLoading
? Center(child: CircularProgressIndicator())
: ElevatedButton(
onPressed: () => _presenter.login('123', '456'),
child: Text('登录'),
),
);
}
void showLoading() => setState(() => _isLoading = true);
void hideLoading() => setState(() => _isLoading = false);
void showError(String message) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
void navigateToHome() => Navigator.of(context).pushReplacementNamed('/home');
}
MVP 的问题是 View 和 Presenter 的接口需要维护,当页面复杂时接口数量会爆炸。
Clean Architecture:洋葱圈架构
Clean Architecture 是 Robert C. Martin 提出的架构模式,核心思想是分层依赖原则:外层依赖内层,内层不知道外层的存在。
Flutter 版本的 Clean Architecture 分层:
lib/
├── core/ # 核心层:工具、常量、错误定义
│ ├── constants/
│ ├── errors/
│ └── utils/
├── features/ # 功能模块(按业务功能划分)
│ └── auth/
│ ├── data/ # 数据层
│ │ ├── datasources/
│ │ ├── models/
│ │ └── repositories/
│ ├── domain/ # 领域层
│ │ ├── entities/
│ │ ├── repositories/
│ │ └── usecases/
│ └── presentation/ # 展示层
│ ├── bloc/
│ ├── pages/
│ └── widgets/
└── injection_container.dart # 依赖注入
这种分层的优势是业务逻辑和框架解耦,换 UI 框架(比如从 Flutter 换成 RN)时 domain 层不需要改。
劣势是层数多,代码量大,中小项目可能过于重量。
实际项目的分层方案
结合实践经验,我目前倾向的方案是三层 + 领域驱动:
lib/
├── core/ # 核心工具和配置
│ ├── api/ # API 客户端封装
│ ├── errors/ # 统一错误处理
│ ├── storage/ # 本地存储
│ └── utils/ # 工具函数
├── shared/ # 跨功能共享
│ ├── widgets/ # 通用组件
│ ├── models/ # 通用数据模型
│ └── providers/ # 全局状态管理
├── features/ # 按业务功能模块
│ ├── auth/
│ │ ├── models/ # 数据模型
│ │ ├── services/ # 业务服务
│ │ ├── providers/ # 状态管理
│ │ └── pages/
│ ├── product/
│ │ ├── models/
│ │ ├── services/
│ │ ├── providers/
│ │ └── pages/
└── main.dart
这个方案比 Clean Architecture 轻,比 MVC 清晰。
数据流设计
分层之后,关键是怎么设计数据流。我现在的做法是:单向数据流 + 依赖注入。
单向数据流
User Action → Provider/Bloc → Service → Repository → DataSource → API/DB
↑ ↓
└────────────────── UI Rebuild ←──────────────────────────────┘
用户操作触发 Provider/Bloc 的方法,Provider/Bloc 调用 Service 执行业务逻辑,Service 调用 Repository 获取数据,数据经过 DataSource 处理后从 API 或本地存储返回,UI 响应状态变化重建。
以商品列表为例:
// 1. Entity:领域实体
class Product {
final String id;
final String name;
final double price;
final String imageUrl;
const Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
});
}
// 2. Repository 接口
abstract class ProductRepository {
Future<List<Product>> getProducts({int page, int pageSize});
Future<Product> getProductById(String id);
}
// 3. Repository 实现
class ProductRepositoryImpl implements ProductRepository {
final ApiClient _apiClient;
ProductRepositoryImpl(this._apiClient);
Future<List<Product>> getProducts({int page = 1, int pageSize = 20}) async {
final response = await _apiClient.get(
'/products',
queryParameters: {'page': page, 'pageSize': pageSize},
);
return (response['data'] as List)
.map((json) => ProductModel.fromJson(json).toEntity())
.toList();
}
Future<Product> getProductById(String id) async {
final response = await _apiClient.get('/products/$id');
return ProductModel.fromJson(response['data']).toEntity();
}
}
// 4. Service:业务逻辑层
class ProductService {
final ProductRepository _repository;
ProductService(this._repository);
Future<List<Product>> getProductList({
required int page,
int pageSize = 20,
String? categoryId,
}) async {
// 可以在这里加缓存逻辑
final products = await _repository.getProducts(
page: page,
pageSize: pageSize,
);
// 业务筛选
if (categoryId != null) {
return products.where((p) => p.categoryId == categoryId).toList();
}
return products;
}
// 业务计算
double calculateDiscount(Product product, UserLevel level) {
switch (level) {
case UserLevel.vip:
return product.price * 0.8;
case UserLevel.svip:
return product.price * 0.7;
case UserLevel.normal:
return product.price;
}
}
}
// 5. Provider:状态管理层
class ProductListProvider extends StateNotifier<ProductListState> {
final ProductService _service;
ProductListProvider(this._service) : super(ProductListState.initial());
Future<void> loadProducts({bool refresh = false}) async {
if (state.isLoading) return;
state = state.copyWith(isLoading: true, isRefreshing: refresh);
try {
final page = refresh ? 1 : state.currentPage + 1;
final products = await _service.getProductList(
page: page,
pageSize: 20,
);
state = state.copyWith(
isLoading: false,
isRefreshing: false,
products: refresh ? products : [...state.products, ...products],
currentPage: page,
hasMore: products.length == 20,
);
} catch (e) {
state = state.copyWith(
isLoading: false,
isRefreshing: false,
error: e.toString(),
);
}
}
}
// 6. UI 层
class ProductListPage extends ConsumerWidget {
const ProductListPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(productListProvider);
if (state.isLoading && state.products.isEmpty) {
return Center(child: CircularProgressIndicator());
}
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is ScrollEndNotification &&
notification.metrics.extentAfter < 200 &&
state.hasMore) {
ref.read(productListProvider.notifier).loadProducts();
}
return false;
},
child: ListView.builder(
itemCount: state.products.length,
itemBuilder: (context, index) {
return ProductCard(product: state.products[index]);
},
),
);
}
}
依赖注入
分层后各层之间有依赖关系,需要统一管理。我倾向用 get_it 做 Service Locator:
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
void setupDependencies() {
// API Client
getIt.registerLazySingleton<ApiClient>(
() => ApiClient(baseUrl: 'https://api.example.com'),
);
// Repositories
getIt.registerLazySingleton<ProductRepository>(
() => ProductRepositoryImpl(getIt<ApiClient>()),
);
// Services
getIt.registerLazySingleton<ProductService>(
() => ProductService(getIt<ProductRepository>()),
);
// Providers
getIt.registerFactory<ProductListProvider>(
() => ProductListProvider(getIt<ProductService>()),
);
}
页面里通过 Provider 获取:
class ProductListPage extends ConsumerWidget {
const ProductListPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return ChangeNotifierProvider(
create: (_) => getIt<ProductListProvider>()..loadProducts(),
child: Scaffold(...),
);
}
}
代码复用策略
分层之后,代码复用变得更容易:
共享 Widget
// shared/widgets/product_card.dart
class ProductCard extends StatelessWidget {
final Product product;
final VoidCallback? onTap;
const ProductCard({
super.key,
required this.product,
this.onTap,
});
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Column(
children: [
CachedNetworkImage(imageUrl: product.imageUrl),
Text(product.name),
Text('¥${product.price}'),
],
),
);
}
}
共享 Model 转换
每个 API 响应都要做 JSON 到 Model 的转换,封装一个 base 类减少重复:
// base model
class BaseModel {
final String id;
final DateTime createdAt;
final DateTime updatedAt;
BaseModel.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['created_at']),
updatedAt = DateTime.parse(json['updated_at']);
}
// 子类 model
class ProductModel extends BaseModel {
final String name;
final double price;
ProductModel.fromJson(Map<String, dynamic> json)
: name = json['name'],
price = (json['price'] as num).toDouble(),
super.fromJson(json);
// 转换为 Entity
Product toEntity() => Product(
id: id,
name: name,
price: price,
);
}
总结
Flutter 项目架构设计的核心原则:
- 分层清晰,依赖单向:上层依赖下层,下层不感知上层
- 数据模型和 UI 模型分离:API 返回的模型(Model)和 UI 使用的实体(Entity)分开
- 业务逻辑统一封装:跨页面复用的业务逻辑放在 Service 层,不要散在 Provider/Bloc 里
- 状态管理统一:选一种状态管理方案用到底,不要混用 Provider、Riverpod、BLoC
- 依赖注入统一管理:用 Service Locator 或 DI 框架,所有依赖在一个地方配置
架构没有银弹,关键是团队一致性和项目匹配度。小团队小项目可以简化分层,专注业务快速落地;大团队大项目需要更严格的分层来保证代码质量和可维护性。
