Coverage HitCount

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 文件包含服务初始化代码,并将根 MyAppProviderScope 包装在一起。
  • 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 键。

关于我

请访问我的 作品集网站 或在 领英 上与我联系。

GitHub

查看 Github