- Easy for developers to understand, nothing too experimental.
- Support multiple developers working on the same codebase.
- Minimize build times.
Use BLoC design pattern
Bloc is a design pattern created by Google to help separate business logic from the presentation layer and enable a developer to reuse code more efficiently.
To archive this, we use a state management library called Bloc was created and maintained by Felix Angelo.
The app has two layers: Data layer
and UI layer
(called presentation)
The architecture follows a reactive programming model with unidirectional data flow:
- User trigger an event (press button on screen .etc.)
- The
View
will send that event tobloc
bloc
handle the event and request corresponding data fromrepository
repository
request data fromdata provider
data provider
is the data source, could fromcache
orremote
. After got the data, return torepository
repository
could do some logic like sync data and return the requested data tobloc
.bloc
updates the state.- While
view
is observingbloc
, it will receive the state corresponding to the sent event and update the interface
For best maintainability, I decided to use four types of models in the app:
- Cache Entity: model to read & write in cache
- Remote DTO: model parsed from network
- Domain Model: app model, nearly don't change
- Ui Data: display data in ui
To analyze this approach:
Pros:
- Code is clearer when the model is decoupled by layers
- Easily maintain the codebase, especially case data from network change too often
- Ui data helps us to increase performance better 🤫
Cons:
- Need to write more code
So above is the concept of this architecture. But how is it actually implemented in each layer?
- models: Dto models
- exceptions: handle all exceptions from server. Example when server response with a incorrect status, such as 404, 503...
- apis: call api here, using Dio for making network request
Future<List<SampleDto>> fetchCompute() async {
final Response response = await _dio.request("/photos");
return JsonExtensions.toObjectsCompute<SampleDto>(
response.data.toString(),
(json) => SampleDto.fromJson(json),
);
}
The JsonExtensions
is a utility class help convert json to corresponding model in background or not.
- models: entity models
- storage: get cache data here. Currently I'm prefer using SharedPreferences, because web platform doesn't support sqlite.
/// save data in json format
Future<bool> saveSamples(List<SampleEntity> samples) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return await prefs.setString(_samplesKey, jsonEncode(samples));
}
/// get data from cache
Future<List<SampleEntity>> getSamples() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return JsonExtensions.toObjects(
prefs.getString(_samplesKey).orEmpty(),
(json) => SampleEntity.fromJson(json),
);
}
Get the data from cache or remote then map to domain model
Future<Result<List<SampleModel>>> fetch() async {
return Result.guardFuture(
() async => (await _sampleApiClient.fetchCompute()).mapToModel(),
_exceptionHandler,
);
}
The Result.guardFuture()
simply wraps Future
with try catch. Also we have Result.guard()
does the same thing but not Future
. See more in result.dart
class
The mapToModel
is an extension, put in the Dto model
extension _Mapper on SampleDto {
SampleModel? mapToModel() {
if (id == null || albumId == null) return null;
return SampleModel(
id: id!,
albumId: albumId!,
title: title.orEmpty(),
url: url.orEmpty(),
thumbnailUrl: thumbnailUrl.orEmpty(),
);
}
}
extension ListMapper on List<SampleDto> {
List<SampleModel> mapToModel() {
return map((e) => e.mapToModel()).whereType<SampleModel>().toList();
}
}
This layer doesn't have much to say. You can deploy as you like, but for consistency try to follow the following package organization:
- feature_name/
logic/
cubits, blocks, eventmodels/
ui dataview/
your screen/page (for consistency usepage
for naming view)widgets/
widgets use for page
Use onGenerateRoute
for setting up routes. All available routes will be declared in AppRouter
.
Add new route:
/// create const route's name
static const String sample = 'sample';
/// declare in onGenerateRoute
static Route onGenerateRoute(RouteSettings settings) {
...
case sample:
return MaterialPageRoute(
builder: (_) => const SamplePage(),
);
...
}
Navigate:
Navigator.of(context).pushNamed(AppRouter.sample);
DS organization in core/ui
folder.
- Follow offical guidance
package
try not use "_". Examplehome_tracking
can be replaced withtracking/home
- Util extension end with
_ext
. Examplejson_ext
- bloc event end with
Event
. ExampleSampleRequestDataEvent
,SampleClearRequestedDataEvent
- bloc state end with
State
. ExampleSampleState
,SampleLoadingState
- models:
- domain model: end with Model (SampleModel)
- remote model: end with Dto (SampleDto)
- cache model: end with Entity (SampleEntity)
- ui model: end with UiData (SampleUiData)
get dependencies
flutter packages get
run build runner for generating needed classes
flutter pub run build_runner build --delete-conflicting-outputs
- Base data flow using BLoC pattern
- Basic usages of bloc to manage app state
- Handle exceptions from API
- [Mobile] Json decode in background
- [Web] Json decode in background
- Network interceptor
- Design system: snackbar
- Design system: typography
- Design system: colors, theme
- Add useful extensions for String based on kotlin
- Add useful extensions for List based on kotlin
- Refactoring a lotttt when I have more experience with Flutter 🥺