Flutter Boilerplate Project
A boilerplate project created in flutter using Bloc, Retrofit. Depend on code generation.
Features
- State management and examples
- Api integration and examples
- Local database and examples
- Code generation
- Local storage
- Logging
- Routing
- Dependency Injection
- Crashlytics template
- DarkTheme
- Multi languages
- Unit tests
- Clean architecture
- Flutter CI
Some packages:
- Freezed
- Flutter Bloc
- Flutter gen
- Retrofit
- Dio
- Bloc test
- Mockito
- Go router
- Dependency Injection
- Logger
- Floor
- SharedPreferences
Getting Started
The Boilerplate contains the minimal implementation required to create a new library or project. The repository code is preloaded with some basic components like basic app architecture, app theme, constants and required dependencies to create a new project. By using boiler plate code as standard initializer, we can have same patterns in all the projects that will inherit it. This will also help in reducing setup & development time by allowing you to use same code pattern and avoid re-writing from scratch.
Up-Coming Features:
- Handle multi bloc event in the same time by bloc concurrency example
- Load more infinite list using bloc example
- Authentication template
Architecture
How to Use
Step 1:
Fork, download or clone this repo by using the link below:
https://github.com/zeref278/flutter_boilerplate.git
Step 2:
Go to project root and execute the following command in terminal to get the required dependencies and generate languages, freezed, flutter gen:
flutter pub get
flutter pub run intl_utils:generate
flutter pub run build_runner build --delete-conflicting-outputs
Step 3:
Go to /packages/rest_client
and execute the following command in terminal to generate model and api client:
flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs
Whenever change freezed file, assets, api
Run command
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:
Create a immutable Model with any features available
- Define a
constructor
+ theproperties
- Override
toString
, operator==
, hashCode - Implement a
copyWith
method to clone the object - Handling
de/serialization
Example
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);
}
Implement
final DogImage dogImage = DogImage.fromJson(json);
///
final DogImage dogImage = dogImage.copyWith(status: 'failed');
/// Deep copy, equal operator ...
...
Retrofit:
Create a api client by code generation, you do not need to implement each request manually
Example
part 'dog_api.g.dart';
@RestApi()
abstract class DogApiClient {
factory DogApiClient(Dio dio, {String baseUrl}) = _DogApiClient;
@GET('/breeds/image/random')
Future<DogImage> getDogImageRandom();
}
Generate to
///
@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;
}
And this api client will use the baseUrl
from a Dio injector
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 and Bloc tests:
If a bloc that you want to test have a required dependencies, you must add it into annotations @GenerateMocks
in /test/app_test/app_test.dart
:
@GenerateMocks([
DogImageRandomRepository,
LogService,
/// TODO
])
void main() {}
Run the following command to generate a mock dependency
flutter pub run build_runner build --delete-conflicting-outputs
Write a test file:
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,
),
],
);
});