Flutter样板项目
一个使用 Bloc、Retrofit 在 Flutter 中创建的样板项目。依赖于代码生成。
功能
- 状态管理和示例
- API集成和示例
- 本地数据库和示例
- 代码生成
- 本地存储
- 日志记录
- 路由
- 依赖注入
- Crashlytics模板
- 深色主题
- 多语言
- 单元测试
- 整洁架构
- Flutter CI
一些包
- Freezed
- Flutter Bloc
- Flutter gen
- Retrofit
- Dio
- Bloc测试
- Mockito
- Go router
- 依赖注入
- Logger
- Floor
- SharedPreferences(共享偏好设置)
入门
样板代码包含创建新库或项目所需的最少实现。存储库代码预加载了一些基本组件,如基本应用架构、应用主题、常量和创建新项目所需的依赖项。通过使用样板代码作为标准初始化器,我们可以在所有继承它的项目中拥有相同的模式。通过允许您使用相同的代码模式并避免从头重写,这也有助于减少设置和开发时间。
即将推出功能
- 在同一时间处理多个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,
),
],
);
});