一、引言:为什么 Flutter 项目也需要架构?
很多初学者认为 Flutter 只是“写 UI 的框架”,只要会用 Column、Row、ListView 就能开发 App。然而,当项目规模扩大、团队协作增多、业务逻辑复杂化后,缺乏清晰架构的代码会迅速陷入“面条式混乱”:
- 状态散落在各个 Widget 中,难以追踪;
- 业务逻辑与 UI 强耦合,复用性差;
- 测试覆盖率低,修改一处引发多处崩溃;
- 新成员上手困难,维护成本飙升。
因此,构建一个可维护、可测试、可扩展的 Flutter 项目架构,是专业开发者的必修课。
本文将系统讲解:
- Flutter 中主流的架构模式(MVC、MVVM、Bloc、Riverpod)对比;
- 如何基于 Clean Architecture + Riverpod 构建企业级项目结构;
- 实战:从零搭建一个天气查询 App 的工程骨架;
- 代码分层规范与最佳实践。
二、Flutter 架构模式全景图
2.1 MVC(Model-View-Controller)
- Model:数据模型(如网络请求、数据库)。
- View:UI 层(Widget)。
- Controller:协调 Model 与 View(通常由 Stateful Widget 扮演)。
❌ 问题:在 Flutter 中,Stateful Widget 既是 View 又是 Controller,导致职责不清,难以单元测试。
2.2 MVVM(Model-View-ViewModel)
- ViewModel:暴露可观察的数据流(如
Stream或ValueNotifier)。 - View:监听 ViewModel 变化并重建 UI。
✅ 优点:分离 UI 与逻辑,支持响应式编程。
⚠️ 缺点:需配合状态管理库(如 Provider)使用。
2.3 Bloc(Business Logic Component)
由 Felix Angelov 提出,核心思想:事件(Event)驱动状态(State)变化。
1abstract class WeatherEvent {}
2class FetchWeather extends WeatherEvent {
3 final String city;
4 FetchWeather(this.city);
5}
6
7abstract class WeatherState {}
8class WeatherInitial extends WeatherState {}
9class WeatherLoading extends WeatherState {}
10class WeatherLoaded extends WeatherState {
11 final WeatherModel weather;
12 WeatherLoaded(this.weather);
13}
Bloc 使用 mapEventToState 将事件映射为状态流:
1class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
2 final WeatherRepository repo;
3
4 WeatherBloc(this.repo) : super(WeatherInitial()) {
5 on<FetchWeather>((event, emit) async {
6 emit(WeatherLoading());
7 try {
8 final weather = await repo.getWeather(event.city);
9 emit(WeatherLoaded(weather));
10 } catch (e) {
11 // 错误处理
12 }
13 });
14 }
15}
✅ 优点:逻辑集中、易于测试、支持时间旅行调试。
❌ 缺点:样板代码多,学习曲线陡。
2.4 Riverpod:现代化状态管理方案
Riverpod 是 Provider 的升级版,由同一作者开发,解决 Provider 的诸多痛点:
- 编译时安全:Provider 引用错误在运行时才暴露,Riverpod 在编译时报错。
- 无上下文依赖:可在任意位置读取状态,无需
BuildContext。 - 支持异步、组合、覆盖(override)等高级特性。
1// 定义一个 provider
2final weatherProvider = FutureProvider.autoDispose.family<WeatherModel, String>((ref, city) async {
3 final repo = ref.watch(weatherRepoProvider);
4 return repo.getWeather(city);
5});
6
7// 在 Widget 中使用
8Consumer(
9 builder: (context, ref, child) {
10 final weatherAsync = ref.watch(weatherProvider('Beijing'));
11 return weatherAsync.when(
12 loading: () => CircularProgressIndicator(),
13 error: (err, _) => Text('Error: $err'),
14 data: (weather) => Text('Temp: ${weather.temp}°C'),
15 );
16 },
17)
✅ 推荐选择:对于新项目,Riverpod + AsyncNotifier(新版 API) 是当前最优雅的方案。
三、Clean Architecture 在 Flutter 中的落地
3.1 什么是 Clean Architecture?
由 Robert C. Martin 提出,核心思想:依赖方向指向内层,外层(UI、DB、网络)依赖内层(业务逻辑)。
分层结构:
1lib/
2├── core/ # 跨层通用工具(exceptions, constants, utils)
3├── features/ # 按功能模块划分(weather, auth, profile...)
4│ └── weather/
5│ ├── data/ # 数据层(API, DB, models)
6│ ├── domain/ # 领域层(entities, repositories, use cases)
7│ └── presentation/ # 表现层(widgets, blocs/providers)
8└── main.dart # 应用入口
3.2 各层职责详解
Domain 层(核心业务)
- Entities:纯业务对象,不依赖任何外部库。
1class Weather { 2 final double temp; 3 final String city; 4 Weather({required this.temp, required this.city}); 5} - Repositories:抽象接口,定义数据获取方式。
1abstract class WeatherRepository { 2 Future<Weather> getWeather(String city); 3} - Use Cases:封装具体业务逻辑(可选,小型项目可省略)。
1class GetWeather { 2 final WeatherRepository repo; 3 GetWeather(this.repo); 4 Future<Weather> call(String city) => repo.getWeather(city); 5}
Data 层(实现细节)
- Models:与 API 或 DB 对应的数据结构(可含 JSON 序列化)。
1class WeatherModel { 2 final double temperature; 3 final String cityName; 4 // fromJson / toJson 5} - Data Sources:真实数据源(如
WeatherRemoteDataSource)。 - Repository 实现:
1class WeatherRepositoryImpl implements WeatherRepository { 2 final WeatherRemoteDataSource remoteDataSource; 3 WeatherRepositoryImpl(this.remoteDataSource); 4 5 @override 6 Future<Weather> getWeather(String city) async { 7 final model = await remoteDataSource.fetch(city); 8 return Weather(temp: model.temperature, city: model.cityName); 9 } 10}
Presentation 层(UI 与状态)
- Providers / Notifiers:桥接 Domain 与 UI。
- Widgets:纯渲染组件,不包含业务逻辑。
四、实战:搭建天气 App 工程骨架
我们将使用 Riverpod + Clean Architecture 构建一个天气查询 App。
4.1 项目初始化
1flutter create flutter_weather_app
2cd flutter_weather_app
添加依赖(pubspec.yaml):
1dependencies:
2 flutter:
3 sdk: flutter
4 flutter_riverpod: ^2.5.1
5 dio: ^5.4.0
6 equatable: ^2.0.5
7 json_annotation: ^4.8.1
8
9dev_dependencies:
10 build_runner: ^2.4.8
11 json_serializable: ^6.7.1
4.2 目录结构:
1lib/
2├── core/
3│ ├── constants/
4│ │ └── app_constants.dart
5│ └── exceptions/
6│ └── network_exceptions.dart
7├── features/
8│ └── weather/
9│ ├── data/
10│ │ ├── datasources/
11│ │ │ └── weather_remote_data_source.dart
12│ │ ├── models/
13│ │ │ └── weather_model.dart
14│ │ └── repositories/
15│ │ └── weather_repository_impl.dart
16│ ├── domain/
17│ │ ├── entities/
18│ │ │ └── weather.dart
19│ │ ├── repositories/
20│ │ │ └── weather_repository.dart
21│ │ └── usecases/
22│ │ └── get_weather.dart
23│ └── presentation/
24│ ├── providers/
25│ │ └── weather_notifier.dart
26│ └── widgets/
27│ ├── weather_screen.dart
28│ └── weather_card.dart
29└── main.dart
4.3 核心代码实现
Domain 层
1// lib/features/weather/domain/entities/weather.dart
2import 'package:equatable/equatable.dart';
3
4class Weather extends Equatable {
5 final double temp;
6 final String city;
7
8 const Weather({required this.temp, required this.city});
9
10 @override
11 List<Object> get props => [temp, city];
12}
1// lib/features/weather/domain/repositories/weather_repository.dart
2abstract class WeatherRepository {
3 Future<Weather> getWeather(String city);
4}
Data 层
1// lib/features/weather/data/models/weather_model.dart
2import 'package:json_annotation/json_annotation.dart';
3import '../../domain/entities/weather.dart';
4
5part 'weather_model.g.dart';
6
7@JsonSerializable()
8class WeatherModel {
9 @JsonKey(name: 'main')
10 late MainData main;
11 @JsonKey(name: 'name')
12 late String cityName;
13
14 Weather toEntity() => Weather(temp: main.temp, city: cityName);
15}
16
17@JsonSerializable()
18class MainData {
19 late double temp;
20}
21
22// 运行 build_runner 生成序列化代码
23// flutter pub run build_runner build --delete-conflicting-outputs
1// lib/features/weather/data/datasources/weather_remote_data_source.dart
2import 'package:dio/dio.dart';
3import '../models/weather_model.dart';
4
5class WeatherRemoteDataSource {
6 final Dio dio = Dio();
7
8 Future<WeatherModel> fetch(String city) async {
9 final response = await dio.get(
10 'https://api.openweathermap.org/data/2.5/weather',
11 queryParameters: {
12 'q': city,
13 'appid': 'YOUR_API_KEY', // 替换为你的 API Key
14 'units': 'metric',
15 },
16 );
17 return WeatherModel.fromJson(response.data);
18 }
19}
1// lib/features/weather/data/repositories/weather_repository_impl.dart
2import '../../domain/entities/weather.dart';
3import '../../domain/repositories/weather_repository.dart';
4import '../datasources/weather_remote_data_source.dart';
5import '../models/weather_model.dart';
6
7class WeatherRepositoryImpl implements WeatherRepository {
8 final WeatherRemoteDataSource remoteDataSource;
9
10 WeatherRepositoryImpl({required this.remoteDataSource});
11
12 @override
13 Future<Weather> getWeather(String city) async {
14 final model = await remoteDataSource.fetch(city);
15 return model.toEntity();
16 }
17}
Presentation 层
1// lib/features/weather/presentation/providers/weather_notifier.dart
2import 'package:flutter_riverpod/flutter_riverpod.dart';
3import '../../../domain/usecases/get_weather.dart';
4
5class WeatherNotifier extends AsyncNotifier<Weather> {
6 @override
7 Future<Weather> build() async => throw UnimplementedError();
8
9 Future<void> fetchWeather(String city) async {
10 final useCase = ref.read(getWeatherProvider);
11 state = const AsyncLoading();
12 state = await AsyncValue.guard(() async => await useCase(city));
13 }
14}
15
16final weatherNotifierProvider = AsyncNotifierProvider<WeatherNotifier, Weather>(
17 WeatherNotifier.new,
18);
1// lib/features/weather/presentation/widgets/weather_screen.dart
2import 'package:flutter/material.dart';
3import 'package:flutter_riverpod/flutter_riverpod.dart';
4import '../providers/weather_notifier.dart';
5
6class WeatherScreen extends ConsumerWidget {
7 @override
8 Widget build(BuildContext context, WidgetRef ref) {
9 final weatherState = ref.watch(weatherNotifierProvider);
10
11 return Scaffold(
12 appBar: AppBar(title: Text('Weather App')),
13 body: Center(
14 child: weatherState.when(
15 loading: () => CircularProgressIndicator(),
16 error: (err, _) => Text('Error: $err'),
17 data: (weather) => Column(
18 mainAxisAlignment: MainAxisAlignment.center,
19 children: [
20 Text('City: ${weather.city}'),
21 Text('Temperature: ${weather.temp}°C'),
22 ],
23 ),
24 ),
25 ),
26 floatingActionButton: FloatingActionButton(
27 onPressed: () {
28 ref.read(weatherNotifierProvider.notifier).fetchWeather('Beijing');
29 },
30 child: Icon(Icons.refresh),
31 ),
32 );
33 }
34}
依赖注入(Provider 容器)
1// lib/main.dart
2void main() {
3 runApp(
4 ProviderScope(
5 overrides: [
6 // 注入 Repository 实现
7 weatherRepositoryProvider.overrideWith(
8 () => WeatherRepositoryImpl(
9 remoteDataSource: WeatherRemoteDataSource(),
10 ),
11 ),
12 ],
13 child: MyApp(),
14 ),
15 );
16}
17
18class MyApp extends StatelessWidget {
19 @override
20 Widget build(BuildContext context) {
21 return MaterialApp(
22 home: WeatherScreen(),
23 );
24 }
25}
五、工程化最佳实践
-
命名规范:
- 文件名:
snake_case(如weather_notifier.dart) - 类名:
PascalCase(如WeatherNotifier) - 变量:
camelCase
- 文件名:
-
避免在 Widget 中直接调用 Repository
所有数据获取必须通过 UseCase 或 Notifier。 -
使用
freezed+riverpod_generator减少样板代码(进阶) -
测试策略:
- Domain 层:单元测试(mock repository)
- Data 层:集成测试(mock Dio)
- Presentation 层:Widget 测试(mock provider)
六、总结
本文详细介绍了如何在 Flutter 中应用 Clean Architecture 与 Riverpod 构建可维护的项目结构。通过分层设计,我们实现了:
- 业务逻辑与 UI 解耦
- 依赖注入与可测试性
- 代码高内聚、低耦合
下一篇文章,我们将深入 Riverpod 高级用法与状态管理实战,包括异步加载、错误处理、状态持久化等。
💬 互动提问:你在 Flutter 网络请求中遇到过哪些坑?欢迎评论区交流!
❤️ 如果本文对你有帮助,请点赞、收藏、转发支持原创!
686

被折叠的 条评论
为什么被折叠?



