Clean Architecture in Flutter | MVVM | BloC | Dio
Clean Architecture is a software design paradigm introduced by Robert C. Martin, and it aims to create maintainable and scalable software by organizing the codebase into distinct layers with clear dependencies and responsibilities. The philosophy is based on several principles, including separation of concerns (SoC), dependency inversion, and the single responsibility principle.
In the dynamic world of mobile app development, creating strong, easy-to-maintain, and scalable applications is a constant goal. Flutter, known for its user-friendly UI toolkit and responsive framework, is popular for building visually appealing cross-platform apps. As Flutter projects become more intricate, the need for a structured architecture becomes crucial for long-term success and manageability.
In this article, we’ll dive into Clean Architecture in the context of Flutter, examining its principles and its impact on the development process. From the core entities to the layers interacting with Flutter’s UI, we’ll see how Clean Architecture promotes maintainability, testability, and flexibility — key factors for the success of any Flutter project.
Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in an inner circle. That includes functions, classes, variables, or any other named software entity.
So, fasten your seatbelts as we venture into the world of Clean Architecture in Flutter, where the code becomes a well-orchestrated symphony, each component playing its part in harmony, creating apps that stand the test of time. 🔥🚀
What is MVVM?
MVVM stands for Model-View-ViewModel, and it is a design pattern commonly used in software development, particularly in the context of user interface (UI) development. MVVM is often associated with frameworks that support data binding, where changes in the UI automatically update the underlying data and vice versa.
In Flutter, the MVVM is not as strictly defined as in some other frameworks that natively support data binding. However, developers often adopt MVVM principles in Flutter by structuring their code in a way that separates concerns, isolates presentation logic, and promotes maintainability.
Here’s a practical guide to implementing MVVM principles in Flutter:
• Model
The Model represents the application’s data and business logic. It is responsible for managing the data and ensuring the consistency and integrity of the application. In the context of MVVM, the Model is often independent of the user interface and is designed to be reusable across different presentation layers.
In Flutter, the model typically consists of Dart classes or objects representing the data and business logic of your application. These classes encapsulate the application’s state and functionality. They don’t directly interact with the UI.
Example
class User {
String name;
int age;
User({required this.name, required this.age});
}
• View
The View is responsible for presenting the data to the user and capturing user interactions. It is the user interface that users interact with. In MVVM, the View is kept as lightweight as possible and is primarily concerned with displaying information. It observes changes in the ViewModel and updates the UI accordingly.
In Flutter, the view is represented by widgets. Widgets are responsible for rendering UI elements and capturing user interactions. Keep your widgets as “dumb” as possible, minimizing logic in the UI components.
class UserView extends StatelessWidget {
final User user;
UserView({required this.user});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(user.name),
subtitle: Text('Age: ${user.age}'),
);
}
}
• ViewModel
The ViewModel is the intermediary between the Model and the View. It contains the presentation logic, exposing data and commands that the View can bind to. The ViewModel is designed to be testable independently of the UI. It also often encapsulates the state of the View and handles user input and interactions.
While Flutter doesn’t have a native ViewModel, you can create Dart classes to act as ViewModels. ViewModels contain the presentation logic, handle data transformations, and provide a clean API for the UI to interact with the data.
Example
import 'dart:async';
class UserViewModel {
final StreamController<User> _userController = StreamController<User>();
Stream<User> get userStream => _userController.stream;
// Business logic and data transformation
void updateUserAge(User user, int newAge) {
final updatedUser = User(name: user.name, age: newAge);
_userController.add(updatedUser);
}
// Dispose the controller to avoid memory leaks
void dispose() {
_userController.close();
}
}
Connecting View and ViewModel
In your Flutter application, you can use state management solutions like Provider, Riverpod, or even simple StatefulWidget to connect the View and ViewModel. These solutions help manage the state and notify the UI when data changes.
Example using Provider:
class UserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final userViewModel = Provider.of<UserViewModel>(context);
return StreamBuilder<User>(
stream: userViewModel.userStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return UserView(user: snapshot.data!);
} else {
return CircularProgressIndicator();
}
},
);
}
}
In-depth explanation of Clean Architecture with Flutter
Let’s now talk about clean architecture details in Flutter, and we will explain and drop the details of the clean architecture on the Flutter.
Clean Architecture provides a way to structure applications that separate the different components of an application into modules, each with a well-defined purpose. The main idea behind Clean Architecture is to separate the application into three main layers: the presentation layer, the domain layer, and the data layer.
The View layer in MVVM represents the Presentation layer in Flutter Clean Architecture, ViewModel represents the Domain layer, and the Model layer represents the Data layer.
The project folder structure will be like this:
features
folder will contain all app features such as auth, profile…, and each feature of the application is built on the basis of the previous three layers (Presentation, Domain, Data), for example the login use-case feature will contains these folders:
The core
folder is a fundamental module housing key components like utils, routes, network, services, validators, and styles, and others. Its content can be tailored by developers to improve code cleanliness and adapt to evolving project needs, ensuring simplicity, modularity, and ease of maintenance.
injection
file containe all injection methods feature, and it is called from the main.dart
.
final sl = GetIt.instance;
Future<void> initInjections() async {
await initSharedPrefsInjections();
await initAppInjections();
await initDioInjections();
await initArticlesInjections();
}
We will see about initArticlesInjections later.
And we call it in the main.dart
file:
// Inject all dependencies
await initInjections();
runApp(DevicePreview(
builder: (context) {
return const App();
},
enabled: false,
));
finally, the shared
folder, it is like features folder but for common feature in our application, like payment feature, shared pages, shared widgets, abd others.
Absolutely, you’ve captured a crucial point. The outlined structure isn’t a rigid standard but rather a foundation that developers can customize based on their project’s requirements and their team’s preferences. It’s essential to be flexible and adapt the architecture to ensure the code remains understandable, maintainable, and conducive to efficient development. Developers are encouraged to modify, extend, or add sections as needed to enhance the overall development process and align with the project’s unique characteristics.
Let’s start with Clean Architecture layers.
1- Presentation Layer
Responsibility
The Presentation Layer is the outermost layer, responsible for presenting information to the user and capturing user interactions. It includes all the components related to the user interface (UI), such as widgets, screens, and presenters/controllers (State Management).
Components
- Screens: Represent the feature screens.
- Widgets and UI Components: Represent the visual elements of the application.
- Manager/Controllers: Contain the presentation logic that interacts with the UI components. They receive user input, communicate with the Use Cases in the Domain Layer, and update the UI accordingly.
The Manager/Controllers layer can incorporate any state management solution, such as BloC, Riverpod, Provider, and others.
Example
2- Domain Layer
Responsibility
The Domain Layer, also known as the Business Logic or Use Case Layer, contains the core business rules and logic of the application. It represents the heart of the software system, encapsulating the essential functionality that is independent of any particular framework.
Components
- Entities: Represent the fundamental business objects or concepts.
- Use Cases: Contain application-specific business rules and orchestrate the flow of data between entities. They are responsible for executing specific actions or operations.
- Business Rules and Logic (Repository): Core functionality that is crucial to the application’s domain.
Example
3- Data Layer
Responsibility
The Data Layer is responsible for interacting with external data sources, such as databases, network services, or repositories. It handles the storage and retrieval of data.
Components
- Repositories or Gateways: Abstract interfaces that define how data is accessed and stored.
- Data Models: Represent the structure of the data as it is stored in the external data sources.
- Data Sources: Implementations of repositories that interact with databases, APIs, or other external services.
In Clean Architecture, dependencies flow inward, meaning that the inner layers (Domain Layer) are independent of the outer layers (Presentation and Data Layers). The inner layers define the core business rules, while the outer layers contain the implementation details.
The key benefit of organizing a system in this layered manner is that it promotes separation of concerns, modularity, and testability. Each layer has a distinct responsibility, and changes to one layer should not affect the others. This architectural style allows for flexibility and adaptability over time.
Example
Project Example
We’re developing a Ny Times News app with news fetching and filtering. Using BloC for State Management, Dio for API calls, and json_serializable for parsing json response to our models, we aim to deliver a responsive and seamless user experience.
First, we have to create an account Get Started | Dev Portal (nytimes.com) and follow the steps to create an app and get Api Key.
1- Login to your account and click on Apps
2- Click on New App from top-right page
3- Fill the App Name
4- Enable Most Popular API option.
If you encounter any issues during the API key creation process, you can utilize this key for testing purposes ‘nF2WTVC6ES9SnxES3o0BzPnijV1RMDHl’. However, it is advisable to generate a new key for production or ongoing development to ensure security and best practices.
The base Api URL we will work on it: http://api.nytimes.com/svc/mostpopular/v2/mostviewed/
Article Api: http://api.nytimes.com/svc/mostpopular/v2/mostviewed/all-sections/period.json?api-key=nF2WTVC6ES9SnxES3o0BzPnijV1RMDHl
period values: 1, 7, 30.
Example: http://api.nytimes.com/svc/mostpopular/v2/mostviewed/all-sections/1.json?api-key=nF2WTVC6ES9SnxES3o0BzPnijV1RMDHl
You can find full example on Github, in this project I used:
get_it | Dart Package (pub.dev) for Dependency Injection (DI).
is a service locator package in Flutter, and it provides a simple yet powerful solution for dependency management. In software development, a service locator is a design pattern that allows objects to locate and obtain dependencies or services without being aware of how those dependencies are constructed.
it has a lot features, most one important is registerSingleton, it allows you to register singletons, meaning that there is only one instance of a particular class, and you can lazily load dependencies when needed.
json_serializable | Dart Package (pub.dev)
To generate fromJson and toJson methods.
flutter_bloc | Flutter Package (pub.dev)
State Management and handling the data coming from data source.
dartz | Dart Package (pub.dev)
It is Flutter or Dart package for handling error. This is very suitable when you do clean architecture and want to catch the error or success separately.
The Either type serves as a versatile construct, designed to encapsulate a value that falls into one of two distinct categories: it can either signify a success along with a value of a particular type, or denote a failure with a value of a different type. This construct proves invaluable for managing errors in a structured and type-safe manner, providing a clear and expressive way to handle both successful outcomes and potential failures within a given context.
Note: In this article, we will discuss the important parts of understanding the Clean Architecture, not the entire code. The entire code can be accessed via Github.
Then let’s start, create a new Flutter project, and let’s start to add important packages in pubspec.yaml
file dependencies
and dev_ependencies
:
dependencies:
flutter:
sdk: flutter
# Fetch Data From Api
dio: ^5.4.0
# BloC State Management
flutter_bloc: ^8.1.3
# Json Annotation
json_annotation: ^4.8.1
# Being able to compare objects in Dart
equatable: ^2.0.5
# Handle Success/Error State From Data Source
dartz: ^0.10.1
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.7
json_serializable: ^6.7.1
We will start from Domain layer, first we have to create models for parsing json api response to our models.
Models
We will start working on Domain Layer — Models, create a model for article Json coming from Api, this is the response of our Api:
Now, we proceed to create models that correspond to the aforementioned response
article_model.dart
part 'article_model.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class ArticleModel {
String? uri;
String? url;
int? id;
int? assetId;
String? source;
String? publishedDate;
String? updated;
String? section;
String? subsection;
@JsonKey(name: "nytdsection")
String? nyTdSection;
String? adxKeywords;
String? column;
String? byline;
String? type;
String? title;
String? abstract;
List<String>? desFacet;
List<String>? orgFacet;
List<String>? perFacet;
List<String>? geoFacet;
List<MediaModel>? media;
int? etaId;
ArticleModel(
{this.uri,
this.url,
this.id,
this.assetId,
this.source,
this.publishedDate,
this.updated,
this.section,
this.subsection,
this.nyTdSection,
this.adxKeywords,
this.column,
this.byline,
this.type,
this.title,
this.abstract,
this.desFacet,
this.orgFacet,
this.perFacet,
this.geoFacet,
this.media,
this.etaId});
factory ArticleModel.fromJson(json) =>
_$ArticleModelFromJson(json);
toJson() => _$ArticleModelToJson(this);
static List<ArticleModel> fromJsonList(List? json) {
return json?.map((e) => ArticleModel.fromJson(e)).toList() ?? [];
}
}
media_model.dart
part 'media_model.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class MediaModel {
String? type;
String? subtype;
String? caption;
String? copyright;
int? approvedForSyndication;
@JsonKey(name: "media-metadata")
List<MediaMetaDataModel>? mediaMetadata;
MediaModel(
{this.type,
this.subtype,
this.caption,
this.copyright,
this.approvedForSyndication,
this.mediaMetadata});
factory MediaModel.fromJson(json) => _$MediaModelFromJson(json);
toJson() => _$MediaModelToJson(this);
static List<MediaModel> fromJsonList(List json) {
return json.map((e) => MediaModel.fromJson(e)).toList();
}
}
media_meta_data_model.dart
part 'media_meta_data_model.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class MediaMetaDataModel {
String? url;
String? format;
int? height;
int? width;
MediaMetaDataModel({this.url, this.format, this.height, this.width});
factory MediaMetaDataModel.fromJson(json) =>
_$MediaMetaDataModelFromJson(json);
toJson() => _$MediaMetaDataModelToJson(this);
static List<MediaMetaDataModel> fromJsonList(List json) {
return json.map((e) => MediaMetaDataModel.fromJson(e)).toList();
}
}
In addition, we create the necessary parameters for the articles API:
articles_params.dart
class ArticlesParams {
ArticlesParams({
required this.period,
});
late final int period;
ArticlesParams.fromJson(Map<String, dynamic> json) {
period = json['period'];
}
Map<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_data['period'] = period;
return _data;
}
}
Then the model's folder will be like this:
See all about json_serializable | Dart Package (pub.dev) for parsing json response to our models.
Api
Now we will work on Data Layer — Data Source, after creating a model's files, we will create an Api for fetching articles data.
Initially, we’ll establish an abstract class to define all the methods responsible for fetching data from the API.
absract_article_api.dart
import 'package:articles_app/articles/data/models/article_model.dart';
abstract class AbstractArticleApi {
// Get all article
Future<List<ArticleModel>> getArticles();
}
We implement the abstract class to incorporate the logic for retrieving data from the API.
article_impl_api.dart
class ArticlesImplApi extends AbstractArticleApi {
final Dio dio;
ArticlesImplApi(this.dio);
// Articles Method
@override
Future<ApiResponse<List<ArticleModel>>> getArticles(
NyTimesArticlesParams params) async {
try {
final result = (await dio.get(
getArticlePath(params.period),
));
if (result.data == null)
throw ServerException("Unknown Error", result.statusCode);
return ApiResponse.fromJson<List<ArticleModel>>(
result.data, ArticleModel.fromJsonList);
} on DioError catch (e) {
throw ServerException(handleDioError(e), e.response?.statusCode);
} on ServerException {
rethrow;
} catch (e) {
throw ServerException(e.toString(), null);
}
}
}
Then the data source folder will be like this:
After creating a models and data source from Data layer, no we have to start creating a repositories interface and its implementation to achieve the core functionality that is crucial to the application’s domain.
The repository establishes a connection with the data layer through a data source function, responsible for retrieving data and handling internal logic. for this reason, the repository takes an articlesApi
instance in its constructor and used it to call articles Api.
Repositories
Now in Domain Layer — Repositories inside domain/repositories
folder, create a dart file abstract_articles_repository.dart
:
abstract class AbstractArticlesRepository {
// Gent Ny Times Articles
Future<Either<Failure, List<ArticleModel>>> getNyTimesArticles(
NyTimesArticlesParams params);
}
Next, proceed to the data/repositories
directory to implement the above-mentioned abstract class.
class ArticlesRepositoryImpl extends AbstractArticlesRepository {
final ArticlesImplApi articlesApi;
ArticlesRepositoryImpl(
this.articlesApi,
);
// Gent Ny Times Articles
@override
Future<Either<Failure, List<ArticleModel>>> getNyTimesArticles(
NyTimesArticlesParams params) async {
try {
final result = await articlesApi.getArticles(params);
return Right(result.results ?? []);
} on ServerException catch (e) {
return Left(ServerFailure(e.message, e.statusCode));
}
}
}
We introduce the articlesApi
variable, which is utilized for fetching article data from the API through the data layer.
Then the repositories files will be like this:
Use-Cases
After successfully establishing the repositories layer, the next step involves creating use-case files. These use cases will encapsulate and orchestrate the application-specific business logic, serving as a bridge between the presentation layer and the repositories, ensuring a clean and modular architecture.
Each use case is tasked with executing specific actions or operations, providing a reusable solution that can be utilized across different features without the need to duplicate code for each new functionality.
In domain/usecases
create a dart file articles_usecase.dart
:
class ArticlesUseCase extends UseCase<List<ArticleModel>, ArticlesParams> {
final AbstractArticlesRepository repository;
ArticlesUseCase(this.repository);
@override
Future<Either<Failure, List<ArticleModel>>> call(
ArticlesParams params) async {
final result = await repository.getArticles(params);
return result.fold((l) {
return Left(l);
}, (r) async {
return Right(r);
});
}
}
Congratulations, now you finish creating Data and Domain Layer. 🎉🎉
So far, we have established a Domain layer housing the essential models which parsed and utilized json response to our models. Additionally, we’ve implemented repositories responsible for processing data obtained from the data layer via the data source.
Now, we can proceed to the Presentation layer to construct the user interface, facilitating user interaction with the data through controllers or managers.
Presentation
In this layer, we will craft pages and widgets responsible for invoking the articles data through the defined use cases. This layer acts as the user interface, facilitating interaction with the application’s underlying logic encapsulated in the use cases.
In article
folder we create article_injection.dart
file, it is containing all feature dependency injection for api, repository, usecases.
import 'package:ny_times_app/src/core/network/dio_network.dart';
import 'package:ny_times_app/src/core/utils/injections.dart';
import 'package:ny_times_app/src/features/articles/data/data_sources/remote/articles_impl_api.dart';
import 'data/data_sources/local/articles_shared_prefs.dart';
import 'data/repositories/articles_repo_impl.dart';
import 'domain/usecases/articles_usecase.dart';
initArticlesInjections() {
sl.registerSingleton(ArticlesImplApi(DioNetwork.appAPI));
sl.registerSingleton(ArticlesSharedPrefs(sl()));
sl.registerSingleton(ArticlesUseCase(sl()));
sl.registerSingleton(ArticlesRepositoryImpl(sl()));
}
Then we can call any above class by calling:
getIt.registerSingleton(ArticlesUseCase(sl()));
Now, In presentation/bloc
create a bloc named it ArticlesBloc:
articles_event.dart
part of 'articles_bloc.dart';
abstract class ArticlesEvent {
const ArticlesEvent();
}
// On Fetching Articles Event
class OnGettingArticlesEvent extends ArticlesEvent {
final int period;
final bool withLoading;
OnGettingArticlesEvent(this.period, {this.withLoading = true});
}
articles_state.dart
part of 'articles_bloc.dart';
abstract class ArticlesState {
const ArticlesState();
}
class NyTimesInitial extends ArticlesState {}
// --------------------Start Get Articles States-------------------- //
// Loading Get Ny Times State
class LoadingGetArticlesState extends ArticlesState {}
// Error On Getting Ny Times State
class ErrorGetArticlesState extends ArticlesState {
final String errorMsg;
ErrorGetArticlesState(this.errorMsg);
}
// Success Get Ny Times State
class SuccessGetArticlesState extends ArticlesState {
final List<ArticleModel> articles;
SuccessGetArticlesState(this.articles);
}
// --------------------End Get Articles States-------------------- //
articles_bloc.dart
import 'package:bloc/bloc.dart';
import 'package:ny_times_app/src/core/network/error/failures.dart';
import 'package:ny_times_app/src/core/util/constant/app_constants.dart';
import 'package:ny_times_app/src/features/articles/domain/models/article_model.dart';
import 'package:ny_times_app/src/features/articles/domain/models/articles_params.dart';
import 'package:ny_times_app/src/features/articles/domain/usecases/articles_usecase.dart';
part 'articles_event.dart';
part 'articles_state.dart';
class ArticlesBloc extends Bloc<ArticlesEvent, ArticlesState> {
final ArticlesUseCase articlesUseCase;
// List of articles
ArticlesBloc({required this.articlesUseCase})
: super(LoadingGetArticlesState()) {
on<OnGettingArticlesEvent>(_onGettingArticlesEvent);
}
// Getting articles event
_onGettingArticlesEvent(
OnGettingArticlesEvent event, Emitter<ArticlesState> emitter) async {
if (event.withLoading) {
emitter(LoadingGetArticlesState());
}
final result = await articlesUseCase.call(
ArticlesParams(
period: event.period,
),
);
result.fold((l) {
emitter(ErrorGetArticlesState(l.errorMessage));
}, (r) {
emitter(SuccessGetArticlesState(r));
});
}
}
Fantastic! You’ve set up your BloC to capture user actions, calling the articles use-case, which communicates with the repository layer to fetch data. Subsequently, the repository layer makes a connection with the Data layer via a data source to retrieve information.
Then the bloc folder will be like this:
For UI, we will create a list of articles page and call the articles via bloc.
In the presentation/pages
, let’s create a page named ‘ArticlesPage’ to serve as a user interface component. This page will be responsible for presenting and interacting with the articles data.
We will design a simple user interface to illustrate the process of fetching articles from the UI layer. The complete code for the List View Page can be found on Github, providing a comprehensive reference for implementation.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ny_times_app/src/core/common_feature/presentation/pages/background_page.dart';
import 'package:ny_times_app/src/core/common_feature/presentation/widgets/app_loader.dart';
import 'package:ny_times_app/src/core/common_feature/presentation/widgets/reload_widget.dart';
import 'package:ny_times_app/src/core/translations/l10n.dart';
import 'package:ny_times_app/src/core/util/helper.dart';
import 'package:ny_times_app/src/core/util/injections.dart';
import 'package:ny_times_app/src/features/articles/domain/models/article_model.dart';
import 'package:ny_times_app/src/features/articles/domain/usecases/articles_usecase.dart';
import 'package:ny_times_app/src/features/articles/presentation/bloc/articles_bloc.dart';
import 'package:ny_times_app/src/features/articles/presentation/widgets/article_card_widget.dart';
class ArticlesPage extends StatefulWidget {
const ArticlesPage({Key? key}) : super(key: key);
@override
State<ArticlesPage> createState() => _ArticlesPageState();
}
class _ArticlesPageState extends State<ArticlesPage> {
ArticlesBloc _bloc = ArticlesBloc(articlesUseCase: sl<ArticlesUseCase>());
List<ArticleModel> nyTimesArticles = [];
// Period
int selectedPeriod = 1;
@override
void initState() {
// Call event to get ny times article
callArticles();
super.initState();
}
@override
Widget build(BuildContext context) {
return BackgroundPage(
withDrawer: true,
child: Column(
children: [
// Space
SizedBox(
height: Helper.getVerticalSpace(),
),
// List of articles
Expanded(
child: BlocConsumer<ArticlesBloc, ArticlesState>(
bloc: _bloc,
listener: (context, state) {
if (state is SuccessGetArticlesState) {
nyTimesArticles.clear();
nyTimesArticles = state.articles;
}
},
builder: (context, state) {
if (state is LoadingGetArticlesState) {
return const AppLoader();
} else if (state is ErrorGetArticlesState) {
return ReloadWidget.error(
content: state.errorMsg,
onPressed: () {
callArticles();
},
);
}
// Check if there is no data
if (nyTimesArticles.isEmpty) {
return ReloadWidget.empty(content: S.of(context).no_data);
}
return ListView.builder(
itemCount: nyTimesArticles.length,
itemBuilder: (context, index) {
return ArticleCardWidget(
nyTimesModel: nyTimesArticles[index],
);
},
);
},
),
)
],
),
);
}
// Call articles
callArticles({bool withLoading = true}) {
_bloc.add(
OnGettingArticlesEvent(
selectedPeriod,
withLoading: withLoading,
),
);
}
}
ArticleBloc calss take ArticleUseCase instance, so we can access to the singelton ArticlesUseCase class from get_it injector:
ArticlesBloc _bloc = ArticlesBloc(articlesUseCase: sl<ArticlesUseCase>());
This is called Dependency injection; it is useful when we want to conduct unit tests for each layer by providing mock instances. This ensures efficient testing and allows us to check the functionality of different parts of our application independently.
The conclusive structure takes the form of:
The previous code flow is illustrated in the accompanying image below:
Congratulations, we finish build an article app using clean architecture🚀
Clean Architecture in Flutter can be beneficial for applications, but the degree of its suitability depends on various factors, including the size and complexity of the application. Here are some considerations:
Learning Curve: Clean Architecture introduces additional concepts and layers, which may result in a steeper learning curve, especially for developers new to the architecture.
Complexity Overhead: For very simple apps, the additional layers and abstractions introduced by Clean Architecture may be perceived as unnecessary complexity.
Development Speed: Initially, development might be slower as developers adapt to the Clean Architecture structure.
For small and straightforward Flutter apps, you might also consider simpler architectures or frameworks that prioritize rapid development, such as MVC. Clean Architecture is often more beneficial in larger, more complex applications.
In summary, Clean Architecture can be a good fit for Flutter apps, especially if they are expected to grow or require a high level of maintainability and testability. However, it’s essential to balance the benefits of Clean Architecture with the specific needs and constraints of the project.
In future articles, we will delve into the significance of unit testing, discussing why it is essential and identifying scenarios where its implementation is beneficial. We will explore how to incorporate unit testing into this project, ensuring a robust and reliable codebase.
Now, with the foundation laid out, you can proceed to build a clean, testable, and scalable project with a more professional structure. This organized architecture will contribute to better code maintainability and ease of future development🎉😎
You can find full project feature and code on Github — Source Code, it has additional code like search on articles, more UI customization, Unit Testing.
Thank you for reading this article. I hope you find it useful. Feel free to contact me if you have any questions.
LinkedIn Account for any questions.