Flutter样板项目

一个使用 Bloc、Retrofit 在 Flutter 中创建的样板项目。依赖于代码生成。

功能

  • 状态管理和示例
  • API集成和示例
  • 本地数据库和示例
  • 代码生成
  • 本地存储
  • 日志记录
  • 路由
  • 依赖注入
  • Crashlytics模板
  • 深色主题
  • 多语言
  • 单元测试
  • 整洁架构
  • Flutter CI

一些包

入门

样板代码包含创建新库或项目所需的最少实现。存储库代码预加载了一些基本组件,如基本应用架构、应用主题、常量和创建新项目所需的依赖项。通过使用样板代码作为标准初始化器,我们可以在所有继承它的项目中拥有相同的模式。通过允许您使用相同的代码模式并避免从头重写,这也有助于减少设置和开发时间。

即将推出功能

  • 在同一时间处理多个bloc事件(通过bloc并发示例)
  • 加载更多无限列表(通过bloc示例)
  • 认证模板

架构

如何使用

步骤 1

通过以下链接Fork、下载或克隆此存储库

https://github.com/zeref278/flutter_boilerplate.git

步骤 2
转到项目根目录,在终端中执行以下命令以获取所需的依赖项并生成语言、freezed、flutter gen

flutter pub get
flutter pub run intl_utils:generate
flutter pub run build_runner build --delete-conflicting-outputs

步骤 3
转到/packages/rest_client并在终端中执行以下命令以生成模型和API客户端

flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs

每当更改freezed文件、资源、API时

运行命令

flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs

Folder structure

flutter_boilerplate/
|- asssets/                   (assets)
|- lib/
  |- common/                  (dimens, spacing, theming)
  |- config/                  (flavor config)
  |- data/                    (repository)
  |- features/                (features page)
  |- generated/               (code generation includes localization and assets generation)
  |- injector/                (dependencies injector)
  |- l10n/                    (localization resources
  |- router/                  (routing)
  |- services/                (app services)
  |- utils/                   (app utils)
|- packages/
  |- rest_client/             (api client)
  |- local_database/          (local database)
|- tests/
  |- app_test/                (mock dependencies)
  |- features/                (bloc test features)

Freezed:

创建一个具有任何可用功能的不可变模型

  • 定义一个构造函数 + 属性
  • 重写toString、运算符==、hashCode
  • 实现copyWith方法来克隆对象
  • 处理序列化/反序列化

示例

part 'dog_image.freezed.dart';
part 'dog_image.g.dart';

@Freezed(fromJson: true)
class DogImage with _$DogImage {
  const factory DogImage({
    required String message,
    required String status,
  }) = _DogImage;

  factory DogImage.fromJson(Map<String, dynamic> json) =>
      _$DogImageFromJson(json);
}

实现

final DogImage dogImage = DogImage.fromJson(json);
///
final DogImage dogImage = dogImage.copyWith(status: 'failed');
/// Deep copy, equal operator ...
...

Retrofit:

通过代码生成创建API客户端,您无需手动实现每个请求

示例

part 'dog_api.g.dart';

@RestApi()
abstract class DogApiClient {
  factory DogApiClient(Dio dio, {String baseUrl}) = _DogApiClient;

  @GET('/breeds/image/random')
  Future<DogImage> getDogImageRandom();
}

生成为

  ///
  @override
  Future<DogImage> getDogImageRandom() async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _headers = <String, dynamic>{};
    final _data = <String, dynamic>{};
    final _result =
        await _dio.fetch<Map<String, dynamic>>(_setStreamType<DogImage>(Options(
      method: 'GET',
      headers: _headers,
      extra: _extra,
    )
            .compose(
              _dio.options,
              '/breeds/image/random',
              queryParameters: queryParameters,
              data: _data,
            )
            .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    final value = DogImage.fromJson(_result.data!);
    return value;
  }

此API客户端将使用Dio注入器中的baseUrl

injector.registerLazySingleton<Dio>(
  () {
  /// TODO: custom DIO here
    final Dio dio = Dio(
      BaseOptions(
        baseUrl: AppConfig.baseUrl,
      ),
    );
    if (!kReleaseMode) {
      dio.interceptors.add(
        LogInterceptor(
          requestHeader: true,
          requestBody: true,
          responseHeader: true,
          responseBody: true,
          request: false,
        ),
      );
    }
    return dio;
  },
  instanceName: dioInstance,
);

injector.registerFactory<DogApiClient>(
  () => DogApiClient(
    injector(instanceName: dioInstance),
  ),
);

Mockito和Bloc测试

如果bloc需要测试,您必须将其添加到/test/app_test/app_test.dart中的@GenerateMocks注释中

@GenerateMocks([
  DogImageRandomRepository,
  LogService,

  /// TODO
])
void main() {}

执行以下命令生成模拟依赖项

flutter pub run build_runner build --delete-conflicting-outputs

编写测试文件

setUp(() {
    bloc = DogImageRandomBloc(
      dogImageRandomRepository: repository,
      logService: logService,
    );
  });

  group('test add event [DogImageRandomRandomRequested]', () {
    blocTest(
      'emit state when success',
      setUp: () {
        when(repository.getDogImageRandom())
            .thenAnswer((_) => Future<DogImage>.value(image));
      },
      build: () => bloc,
      act: (_) => bloc.add(
        const DogImageRandomRandomRequested(),
      ),
      expect: () => [
        isA<DogImageRandomState>().having(
          (state) => state.status,
          'status',
          UIStatus.loading,
        ),
        isA<DogImageRandomState>()
            .having(
              (state) => state.status,
              'status',
              UIStatus.loadSuccess,
            )
            .having(
              (state) => state.dogImage,
              'image',
              image,
            ),
      ],
    );

    blocTest(
      'emit state when failed',
      setUp: () {
        when(repository.getDogImageRandom()).thenThrow(Exception('error'));
      },
      build: () => bloc,
      seed: () => const DogImageRandomState(dogImage: image),
      act: (_) => bloc.add(
        const DogImageRandomRandomRequested(),
      ),
      expect: () => [
        isA<DogImageRandomState>().having(
          (state) => state.status,
          'status',
          UIStatus.loading,
        ),
        isA<DogImageRandomState>()
            .having(
              (state) => state.status,
              'status',
              UIStatus.actionFailed,
            )
            .having(
              (state) => state.dogImage,
              'image',
              image,
            ),
      ],
    );
  });

GitHub

https://github.com/zeref278/flutter_boilerplate