Flutter-Clean Architecture

Semih Altın
5 min readMar 19, 2024

--

What is the Clean Architecture ?

Clean Architecture is a design principle that promotes separation of concerns. Aims to create a modular, scalable and testable code base. Clean Architecture provides instructions on how to structure the code base. It also includes guidelines on how to define dependencies between different layers of an application. It is not specific to Flutter.

The clean architecture consists of three layers. These are the Presentation Layer, Domain Layer and Data Layer. Let’s examine these now.

  1. Presentation Layer:
    The presentation layer is the layer where the user interacts with the application. The presentation layer consists of two main parts. These are Widgets and Presentation Logic Holders. The Widgets section contains widgets, screens and views. In the Presentation Logic Holders section, there are classes such as bloc and getx, where we combine the interface and data.
  2. Domain Layer:
    The Domain Layer represents the business logic of the feature. Domain Layer includes Use Cases, Entities and Repositories. The domain layer should be agnostic of any specific framework or technology.
  3. Data Layer:
    Data layer is the data retrieval and data storage part. The data layer consists of three parts. These are Models, Repositories and Data Sources. The data sources part can be APIs, local databases, or other external data providers.

Why Clean architecture ?

Clean architecture benefits us in many areas. Firstly it facilitates testability. Because the layers can be tested independently. The code becomes easier to make changes and additions as the application grows. The codebase becomes more modular as each layer has different responsibilities.

Filling Example

Let’s imagine an application where users come from API and are listed on the screen. First, let’s create the “features” folder for the features of the application. Then create a “users” folder on the “features” folder and add the folder required for our layers.

I created two folders, bloc and screens, inside the Presentation Layer. Then I created the entities, repository and usecases folder for the Domain Layer. Finally I created data_sources, models and repository folder for the Data Layer.

Code Example

I will explain the Entities, Use Case and Bloc parts with more detail in my other posts. For now, let’s go through the example.

To analyze the state of the data, I created the data_state.dart file in the core/resources/ location.

abstract class DataState<T> {
final T? data;
final FlutterError? error;

const DataState({this.data, this.error});
}

class DataSuccess<T> extends DataState<T> {
const DataSuccess(T data) : super(data: data);
}

class DataFailed<T> extends DataState<T> {
const DataFailed(FlutterError error) : super(error: error);
}

Since this is a simple example, I did not customize the error types. I recommend customizing your error types in your own project.

Then I created my main UseCase. Use cases implement the basic business logic of the project and connect the Presentation and Domain layers.

abstract class UseCase<Type,Params> {
Future<Type> call({Params params});
}

Now we can start coding the layers. Since I have a block and listing screen in my presentation layer, I will not explain it in detail. We can start with the domain layer.

I created the User Entity class in the domain/entity/ for users’ information as follows.

class UserEntity extends Equatable {
final int? id;
final String? firstName;
final String? lastName;
final int? age;

const UserEntity({
this.id,
this.firstName,
this.lastName,
this.age,
});

@override
List <Object?> get props => [id, firstName, lastName, age];
}

We create the UserRepository class and add the necessary functions.

abstract class UserRepository {
Future<DataState<List<UserEntity>>> getUsers();
}

Since I will only be fetching users, I created a UseCase called GetUsersUseCase. Here we call the getUsers() function on the repository.

class GetUsersUseCase implements UseCase<DataState<List<UserEntity>>,void> {
final UserRepository _usersRepository;
GetUsersUseCase(this._usersRepository);

@override
Future<DataState<List<UserEntity>>> call({void params}) {
return _usersRepository.getUsers();
}
}

We have completed the domain layer. First, I create the UserModel for the data layer. UserModel inherits from the UserEntitiy class. Here I added the functions of converting the data coming from the API into a model and converting the model into an entity.

class UserModel extends UserEntity {

const UserModel({
int? id,
String? firstName,
String? lastName,
int? age,
}) : super(
id: id,
firstName: firstName,
lastName: lastName,
age: age,
);

factory UserModel.fromJson(Map<String,dynamic> map) => UserModel(
firstName: map['firstName'],
lastName: map['lastName'],
age: map['age'],
);

factory UserModel.fromEntity(UserEntity entity) => UserModel(
id: entity.id,
firstName: entity.firstName,
lastName: entity.lastName,
age: entity.age,
);
}

UsersRemoteDataSource class is the class from which we retrieve data from the Api. Here we convert the data from the API into a list of models. It would be better to customize the data conversion and error throwing parts according to the API from which you pull the data.

import 'package:http/http.dart' as http;

abstract class UsersRemoteDataSource {
Future<List<UserModel>> getUsers();
}

class UsersRemoteDataSourceImpl extends UsersRemoteDataSource {
@override
Future<List<UserModel>> getUsers() async {
var url = Uri.parse("xxx/getUsers");
Map<String, String> header = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
var response = await http.get(url, headers: header);
if (response.statusCode == 200) {
var data = json.decode(response.body);
List<UserModel> result = data['users'].map<UserModel>((dynamic i) => UserModel.fromJson(i as Map<String, dynamic>)).toList();
return result;
} else {
throw Error();
}
}
}

The UserRepositoryImpl class inherits from the UserRepository class we created previously.

class UserRepositoryImpl implements UserRepository {
final UsersRemoteDataSource _newsRemoteDataSource;
UserRepositoryImpl(this._newsRemoteDataSource);

@override
Future<DataState<List<UserEntity>>> getUsers() async {
try{
var result = await _newsRemoteDataSource.getUsers();
return DataSuccess(result);
} on FlutterError catch(error) {
return DataFailed(error);
}
}
}

Finally, we call GetUsersUseCase in UsersBloc and update the UserState according to the returned result.

part 'users_event.dart';
part 'users_state.dart';

class UsersBloc extends Bloc<UsersEvent,UsersState> {

final GetUsersUseCase _getUsersUseCase;
UsersBloc(this._getUsersUseCase) : super(UsersInitial()) {
on<GetUsers>(getUsers);
}

Future<void> getUsers(event, emit) async {
emit(UsersLoading());
final result = await _getUsersUseCase.call();
if(result.error != null){
emit(UsersLoadFailure(result.error!.message));
} else if(result.data != null){
emit(UsersLoaded(result.data!));
}
}
}

This way we simply created an architect.

😊 Thanks for reading 😊

--

--