DEV Community

DUBYDU
DUBYDU

Posted on

1

Flutter x Clean Architecture

In the scope of this article, I assume that you already know what Clean Architecture is, and why should we use it.

./ Flutter Clean Architecture

Image description
As you can see, there are three main layers: Presentation, Domain, and Data.

Role and description of each layer:

I. Presentation

There is no business logic processing on this layer, hence it is only used to show UI and handle events.

1. Widget (Page)

  • Notify BloC of events such as screen display and user touch events, and Listen to the events emitted from BloC as well.

2. BloC

  • Receive events from View and execute UseCase according to the event if necessary.
  • Emit the data received from UseCase to View.
  • BloC doesn’t know about View at all.

II. Domain

This layer is responsible for business logic.

1. UseCase

  • Describes the logic processing required for the UseCase.
  • Work directly to Repository.

2. Translator

  • Convert the model into the entity in order to request to server-side, or convert the entity into the model to use for Presentation layer.

3. Model

  • A model will not depend on the data that acquire from server-side.
  • A model is used for the Presentation layer.

III. Data

This layer is responsible for communicating with server-side and data management logic.

1. DataSource

  • Describes the process to acquire and update the data.
  • This is the place decided whether to get the data from the server-side or use the data in the DB or cache.

2. Entity

  • An entity will depend on the data that acquire from server-side.
  • An entity is not used for the Presentation layer.

3. Repository

  • The bridge between Data layer and Domain layer.
  • We will use Translator in Repository in order to convert data.

./ Practice

For a better understanding, I’d like to demonstrate a flow that receives an action from Widget, processes it, requests/receives data from the server-side, and finally displays the data to Widget.

Source-code: https://github.com/dubydu/fluttourII

1. Data Layer

  • Create an Entity.
@JsonSerializable(explicitToJson: true)
class FluttourResponse {
FluttourResponse({
this.flutterVersion,
this.dartVersion,
this.devToolsVersion,
this.cocoapodsVersion,
this.frameworkRevision,
});
final String? flutterVersion;
final String? dartVersion;
final String? devToolsVersion;
final String? cocoapodsVersion;
final String? frameworkRevision;
factory FluttourResponse.fromJson(Map<String, dynamic> json) =>
_$FluttourResponseFromJson(json);
Map<String, dynamic> toJson() => _$FluttourResponseToJson(this);
}
@retrofit.GET('/fluttour-doctor')
Future<FluttourResponse> getFluttourDoctor();
view raw api_client.dart hosted with ❤ by GitHub
  • Then, open your terminal, and run this command: make gen, the retrofit will take the rest responsibility. You also can custom the command line by editing this Makefile.
  • Define DataSource
abstract class HomeDataSourceType {
Future<FluttourResponse> getFluttourDoctor();
}
class HomeDataSource implements HomeDataSourceType {
HomeDataSource({required this.apiClient});
final APIClientType apiClient;
@override
Future<FluttourResponse> getFluttourDoctor() async {
return await apiClient.getFluttourDoctor();
}
}
  • Implement Repository
abstract class HomeRepositoryType {
Future<Either<Failure, FluttourResponse>> getFluttourDoctor();
}
class HomeRepository with ConnectivityMixin implements HomeRepositoryType {
HomeRepository({required this.dataSource});
final HomeDataSourceType dataSource;
@override
Future<Either<Failure, FluttourResponse>> getFluttourDoctor() async {
if (await isInConnection()) {
try {
final response = await dataSource.getFluttourDoctor();
return Right(response);
} catch (e) {
return Left(Failure(e.toString()));
}
}
return const Left(NoConnection());
}
}

2. Domain Layer

  • Create Model
class Fluttour extends Equatable {
const Fluttour({
required this.flutterVersion,
required this.dartVersion,
required this.devToolsVersion,
required this.cocoapodsVersion
});
final String? flutterVersion;
final String? dartVersion;
final String? devToolsVersion;
final String? cocoapodsVersion;
@override
List<Object?> get props => [
flutterVersion,
dartVersion,
devToolsVersion,
cocoapodsVersion
];
Map<String, dynamic> toJson() => {
"flutterVersion": flutterVersion,
"dartVersion": dartVersion,
"devToolsVersion": devToolsVersion,
"cocoapodsVersion": cocoapodsVersion,
};
}
view raw fluttour.dart hosted with ❤ by GitHub
  • Define Translator
class HomeTranslator {
/// To Model
static Fluttour toModel({required FluttourResponse response}) {
return Fluttour(
flutterVersion: response.flutterVersion,
dartVersion: response.dartVersion,
devToolsVersion: response.devToolsVersion,
cocoapodsVersion: response.cocoapodsVersion,
);
}
}
  • Implement UseCase
abstract class HomeUseCaseType {
Future<Either<Failure, Fluttour>> getFluttourDoctor();
}
class HomeUseCase implements HomeUseCaseType {
HomeUseCase({required this.repository});
final HomeRepositoryType repository;
@override
Future<Either<Failure, Fluttour>> getFluttourDoctor() async {
final result = await repository.getFluttourDoctor();
return result.map((response) => HomeTranslator.toModel(response: response));
}
}

3. Presentation Layer

  • Define BloC
class HomeBloc extends Cubit<HomeState> {
HomeBloc({
required this.useCase
}) : super(const HomeInitialState());
final HomeUseCaseType useCase;
Future<void> getFluttourDoctor() async {
final response = await useCase.getFluttourDoctor();
response.fold((error) {
log('=============== $error');
}, (response) {
emit(state.copyWith(fluttour: response));
});
}
}
class HomeState extends Equatable {
const HomeState({required this.fluttour});
final Fluttour? fluttour;
HomeState copyWith({
final Fluttour? fluttour,
}) {
return HomeState(
fluttour: fluttour ?? this.fluttour,
);
}
@override
List<Object?> get props => [fluttour];
}
class HomeInitialState extends HomeState {
const HomeInitialState(): super(fluttour: null);
}
view raw home_bloc.dart hosted with ❤ by GitHub
  • Implement Widget
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with ResponsiveMixin, AfterLayoutMixin, RouteAware {
late HomeBloc _homeBloc;
@override
Future<void> afterFirstLayout(BuildContext context) async {
// Init home bloc
_homeBloc = BlocProvider.of(context, listen: false);
// Call fluttour doctor api
await _getFluttourDoctor();
}
Future<void> _getFluttourDoctor() async {
await _homeBloc.getFluttourDoctor();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Subscribe routeAware
global.navigationObserver.subscribe(this, ModalRoute.of(context)!);
}
@override
void didPopNext() async {
// Call fluttour doctor api
await _getFluttourDoctor();
super.didPopNext();
}
@override
void dispose() {
// Unsubscribe routeAware
global.navigationObserver.unsubscribe(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
initResponsive(context);
return BaseMaterialPage(
child: SafeArea(
child: BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
if (state.fluttour != null) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ ],
);
} else {
return SizedBox(
width: 24.sp,
height: 24.sp,
child: const CupertinoActivityIndicator(),
);
}
},
),
)
);
}
}
view raw home_page.dart hosted with ❤ by GitHub

4. DI

Let’s make our code becomes more modular. 😎

  • Create config_module
mixin ConfigModule {
/// App config
AppConfigType get appConfig {
return AppConfig.shared;
}
/// App Secure config
AppSecureConfigType get appSecureConfig {
return AppSecureConfig();
}
}
  • Create client_module
mixin ClientModule on ConfigModule {
/// API Client
APIClientType get apiClient {
return APIClient.apiClient(
baseDomain: appConfig.baseDomain,
authToken: appSecureConfig.authToken
);
}
}
  • Create datasource_module
mixin DatasourceModule on ClientModule {
/// HomeDataSource
HomeDataSourceType get homeDataSource {
return HomeDataSource(apiClient: apiClient);
}
}
  • Create repository_module
mixin RepositoryModule on DatasourceModule {
/// HomeRepository
HomeRepositoryType get homeRepository {
return HomeRepository(dataSource: homeDataSource);
}
}
  • Create usecase_module
mixin UseCaseModule on RepositoryModule {
/// HomeUseCase
HomeUseCase get homeUseCase {
return HomeUseCase(repository: homeRepository);
}
}
  • Inject UseCase to BloC in my_app.dart by conforming to all of the modules.
class MyAppState extends State<MyApp>
with ConfigModule,
ClientModule,
DatasourceModule,
RepositoryModule,
UseCaseModule {
@override
Widget build(BuildContext context) {
ScreenUtil.init(context);
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => HomeBloc(
useCase: homeUseCase
))
],
child: //..
);
}
}
view raw my_app.dart hosted with ❤ by GitHub

That’s all, run the project, and let’s see what happens!

Pitfalls

In order to run this project, you need to run in a specific environment.

Development: flutter run -t lib/main_dev.dart
Production: flutter run -t lib/main_prod.dart

Sentry blog image

The countdown to March 31 is on.

Make the switch from app center suck less with Sentry.

Read more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay