Flutter Clean Architecture with Riverpod
一个使用“Dummy Json”API 的 Flutter 应用。
功能
- 登录
- 获取产品
- 搜索产品
- 分页
本项目使用了什么?
-
Riverpod 用于状态管理
-
Freezed 代码生成
-
Dartz 函数式编程
Either<Left,Right> -
Auto Route 导航包,使用代码生成来简化路由设置
-
Dio Dart 的 Http 客户端。支持拦截器和全局配置
-
Shared Preferences 持久化存储简单数据
-
Flutter 和 Dart 当然还有 Flutter 和 Dart?
项目描述
数据层
数据层是应用程序的最外层,负责与服务器端或本地数据库通信以及数据管理逻辑。它还包含存储库实现。
a. 数据源
描述了获取和更新数据的过程。包括远程和本地数据源。远程数据源将执行 API 的 HTTP 请求。同时,本地数据源将缓存或持久化数据。
b. 存储库
数据层和领域层之间的桥梁。领域层中存储库的实际实现。存储库负责协调来自不同数据源的数据。
领域层
领域层负责所有业务逻辑。它纯粹用 Dart 编写,不包含 Flutter 元素,因为领域应该只关注应用程序的业务逻辑,而不关注实现细节。
a. Providers
描述了应用程序所需的逻辑处理。直接与存储库通信。
b. 存储库
定义外部层预期功能的抽象类。
表现层
表示层是最依赖框架的一层。它负责所有 UI 和处理 UI 中的事件。它不包含任何业务逻辑。
a. Widget (屏幕/视图)
Widgets 通知事件并监听从 StateNotifierProvider 发出的状态。
b. Providers
描述了表示层所需的逻辑处理。直接与领域层中的 Providers 通信。
项目描述
main.dart文件包含服务初始化代码,并将根MyApp与ProviderScope包装在一起。main/app.dart包含根MaterialApp并初始化AppRouter以在整个应用程序中处理路由。services抽象应用程序级服务及其实现。shared文件夹包含跨功能共享的代码。theme包含通用样式(颜色、主题和文本样式)。model包含应用程序所需的所有数据模型。http使用 Dio 实现。storage使用 SharedPreferences 实现。- 服务定位器模式和 Riverpod 用于在其他层中使用时抽象服务。
例如
final storageServiceProvider = Provider((ref) {
return SharedPrefsService();
});
// Usage:
// ref.watch(storageServiceProvider);
features文件夹:存储库模式用于将访问数据源所需的逻辑与领域层解耦。例如,DashboardRepository抽象并集中了从远程获取Product所需的各种功能。
abstract class DashboardRepository {
Future<Either<AppException, PaginatedResponse>> fetchProducts({required int skip});
Future<Either<AppException, PaginatedResponse>> searchProducts({required int skip, required String query});
}
使用 DashboardDatasource 实现存储库。
class DashboardRepositoryImpl extends DashboardRepository {
final DashboardDatasource dashboardDatasource;
DashboardRepositoryImpl(this.dashboardDatasource);
@override
Future<Either<AppException, PaginatedResponse>> fetchProducts(
{required int skip}) {
return dashboardDatasource.fetchPaginatedProducts(skip: skip);
}
@override
Future<Either<AppException, PaginatedResponse>> searchProducts(
{required int skip, required String query}) {
return dashboardDatasource.searchPaginatedProducts(
skip: skip, query: query);
}
}
使用 Riverpod Provider 访问此实现。
final dashboardRepositoryProvider = Provider<DashboardRepository>((ref) {
final datasource = ref.watch(dashboardDatasourceProvider(networkService));
return DashboardRepositoryImpl(datasource);
});
最后,使用 Riverpod StateNotifierProvider 从表示层访问存储库实现。
final dashboardNotifierProvider =
StateNotifierProvider<DashboardNotifier, DashboardState>((ref) {
final repository = ref.watch(dashboardRepositoryProvider);
return DashboardNotifier(repository)..fetchProducts();
});
注意抽象的 NetworkService 是如何从存储库实现中访问的,然后抽象的 DashboardRepository 是如何从 DashboardNotifier 中访问的,以及这些层如何通过提供切换实现和单独更改/测试每个层来达到分离和可伸缩性。
测试
test 文件夹镜像了 lib 文件夹,并包含一些测试实用程序。
state_notifier_test 用于测试 StateNotifier 和模拟 Notifier。
mocktail 用于模拟依赖项。
1. 测试简单的 Provider provider
test('dashboardDatasourceProvider is a DashboardDatasource', () {
dashboardDataSource = providerContainer.read
(dashboardDatasourceProvider(networkService));
expect(
dashboardDataSource,
isA<DashboardDatasource>(),
);
});
下面是如何在 Flutter 之外单独测试它。
void main() {
late DashboardDatasource dashboardDatasource;
late DashboardRepository dashboardRepository;
setUpAll(() {
dashboardDatasource = MockRemoteDatasource();
dashboardRepository = DashboardRepositoryImpl(dashboardDatasource);
});
test(
'Should return AppException on failure',
() async {
// arrange
when(() => dashboardDatasource.searchPaginatedProducts(skip: any(named: 'skip'), query: any(named: 'query')))
.thenAnswer(
(_) async => Left(ktestAppException),
);
// assert
final response = await dashboardRepository.searchProducts(skip: 1, query: '');
// act
expect(response.isLeft(), true);
},
);
}
class MockRemoteDatasource extends Mock implements DashboardRemoteDatasource {}
探索测试覆盖率
运行 bash gencov.sh
文件夹结构
lib
├── configs
│ └── app_configs.dart
│
├── main
│ ├── app.dart
│ ├── app_env.dart
│ ├── main_dev.dart
│ ├── main_staging.dart
│ └── observers.dart
│
├── configs
│ └── app_configs.dart
├── routes
│ ├── app_route.dart
│ └── app_route.gr.dart
│
├── services
│ └── user_cache_service
│ ├── data
│ │ ├── datasource
│ │ │ └── user_local_datasource.dart
│ │ └── repositories
│ │ └── user_repository_impl.dart
│ ├── domain
│ │ ├── providers
│ │ │ └── user_cache_provider.dart
│ │ └── repositories
│ │ └── user_cache_repository.dart
│ └── presentation
│
├── shared
│ ├── data
│ │ ├── local
│ │ │ ├── shared_prefs_storage_service.dart
│ │ │ └── storage_service.dart
│ │ └── remote
│ │ ├── dio_network_service.dart
│ │ ├── network_service.dart
│ │ └── remote.dart
│ ├── domain
│ │ ├── models
│ │ │ ├── product
│ │ │ │ ├── product_model.dart
│ │ │ │ ├── product_model.freezed.dart
│ │ │ │ └── product_model.g.dart
│ │ │ ├── user
│ │ │ │ └── user_model.dart
│ │ │ ├── models.dart
│ │ │ ├── paginated_response.dart
│ │ │ ├── parse_response.dart
│ │ │ └── response.dart
│ │ └── providers
│ │ ├── dio_network_service_provider.dart
│ │ └── sharedpreferences_storage_service_provider.dart
│ ├── exceptions
│ │ └── http_exception.dart
│ ├── mixins
│ │ └── exception_handler_mixin.dart
│ ├── theme
│ │ ├── app_colors.dart
│ │ ├── app_theme.dart
│ │ ├── test_styles.dart
│ │ └── text_theme.dart
│ ├── widgets
│ │ ├── app_error.dart
│ │ └── app_loading.dart
│ └── globals.dart
│
├── features
│ ├── authentication
│ │ ├── data
│ │ │ ├── datasource
│ │ │ │ ├── auth_local_data_source.dart
│ │ │ │ └── auth_remote_data_source.dart
│ │ │ └── repositories
│ │ │ └── atuhentication_repository_impl.dart
│ │ ├── domain
│ │ │ ├── providers
│ │ │ │ └── login_provider.dart
│ │ │ └── repositories
│ │ │ └── auth_repository.dart
│ │ └── presentation
│ │ ├── providers
│ │ │ ├── state
│ │ │ │ ├── auth_notifier.dart
│ │ │ │ ├── auth_state.dart
│ │ │ │ └── auth_state.freezed.dart
│ │ │ └── auth_providers.dart
│ │ ├── screens
│ │ │ └── login_screen.dart
│ │ └── widgets
│ │ ├── auth_field.dart
│ │ └── button.dart
│ ├── dashboard
....
运行此项目
克隆此仓库
git clone https://github.com/Uuttssaavv/flutter-clean-architecture-riverpod
进入项目目录
cd flutter-clean-architecture-riverpod
获取所有包
flutter pub get
运行 build runner 命令
flutter pub run build_runner build
运行项目
flutter run 或如果您使用的是 VSCode,请按 F5 键。