Implementing clean architecture in flutter apps

Santhosh Adiga U
5 min readFeb 27, 2023

--

Flutter is an open-source mobile application development framework created by Google. It has gained popularity among developers due to its fast development speed and cross-platform capabilities. Clean Architecture is an architectural pattern that can be applied to mobile app development to improve code quality, scalability, and maintainability. In this article, we will discuss using Clean Architecture in Flutter with an example.

What is Clean Architecture?

Clean Architecture is an architectural pattern introduced by Uncle Bob (Robert C. Martin), a software engineer and author. It is a way of organizing the code into layers, with each layer having a specific responsibility. The layers are separated by boundaries that protect the inner layers from the outer layers. The key principle of Clean Architecture is to separate the business logic from the implementation details, making the code more modular, testable, and maintainable.

Clean Architecture in Flutter Flutter allows developers to implement Clean Architecture using various libraries and patterns. In this example, we will use the following libraries:

  1. GetX: a state management library that provides reactive programming and dependency injection.
  2. Dio: a network library for making HTTP requests.
  3. Sqflite: a local database library.

The layers in Clean Architecture are as follows:

  1. Presentation Layer: This layer is responsible for displaying the user interface and handling user input. In our example, we will use the GetX library to manage the state of the app and display the UI.
  2. Domain Layer: This layer contains the business logic of the application. It defines the use cases and entities that are used in the app. We will create a folder called ‘domain’ to store the use cases and entities.
  3. Data Layer: This layer is responsible for interacting with external systems such as APIs or databases. We will use the Dio library for making HTTP requests and the Sqflite library for local storage. We will create a folder called ‘data’ to store the repositories that interact with external systems.

Example: A Weather App Let’s create a simple weather app using Clean Architecture in Flutter. The app will display the weather information of a city entered by the user.

Presentation Layer:

Create a folder called ‘presentation’ to store the UI code. We will create a stateless widget called ‘HomePage’ that contains a text field to enter the city name and a button to get the weather information.

class HomePage extends StatelessWidget {
final TextEditingController cityController = TextEditingController();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Weather App')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: cityController,
decoration: InputDecoration(
hintText: 'Enter city name',
),
),
SizedBox(height: 16.0),
ElevatedButton(
onPressed: () {
// TODO: Get weather information
},
child: Text('Get Weather'),
),
],
),
),
);
}
}

We will use the GetX library to manage the state of the app. Create a folder called ‘presentation/controllers’ to store the controllers.

class HomeController extends GetxController {
final WeatherUseCase _weatherUseCase;

HomeController(this._weatherUseCase);

final _weather = Weather.empty().obs;

Weather get weather => _weather.value;

void getWeather(String city) async {
final result = await _weatherUseCase(city);
result.fold(
(failure) => print('Error'),
(weather) => _weather.value = weather,
);
}
}

Domain Layer:

Create a folder called ‘domain’ to store the use cases and entities. We will create an entity called ‘Weather’ that contains the weather information.

class Weather {
final String cityName;
final double temperature;
final String description;

Weather({
required this.cityName,
required this.temperature,
required this.description,
});

factory Weather.empty() {
return Weather(
cityName: '',
temperature: 0.0,
description: '',
);
}
}

Create a use case called ‘WeatherUseCase’ that uses the ‘WeatherRepository’ to get the weather information.

class WeatherUseCase {
final WeatherRepository _repository;

WeatherUseCase(this._repository);

Future<Either<Failure, Weather>> call(String city) async {
return await _repository.getWeather(city);
}
}

Data Layer:

Create a folder called ‘data’ to store the repositories that interact with external systems. We will create a ‘WeatherRepository’ that uses the Dio library to make HTTP requests and the Sqflite library to store the weather information locally.

class WeatherRepository {
final Dio _dio;
final WeatherDao _dao;

WeatherRepository(this._dio, this._dao);

Future<Either<Failure, Weather>> getWeather(String city) async {
try {
final response = await _dio.get(
'https://api.openweathermap.org/data/2.5/weather?q=$city&appid=<YOUR_API_KEY>&units=metric');

final weather = Weather(
cityName: response.data['name'],
temperature: response.data['main']['temp'],
description: response.data['weather'][0]['description'],
);

await _dao.insertWeather(weather);

return Right(weather);
} on DioError catch (e) {
return Left(Failure(e.message));
}
}
}

We will use the ‘sqflite’ package to store the weather information locally. Create a file called ‘weather_dao.dart’ that contains the implementation of the ‘WeatherDao’ class.

class WeatherDao {
final Future<Database> _db;

WeatherDao(this._db);

Future<void> insertWeather(Weather weather) async {
final db = await _db;
await db.insert('weather', weather.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace);
}

Future<Weather> getWeather() async {
final db = await _db;
final maps = await db.query('weather');

if (maps.isNotEmpty) {
return Weather.fromMap(maps.first);
} else {
return Weather.empty();
}
}
}

Finally, we will initialize the dependencies using the ‘GetIt’ package. Create a file called ‘locator.dart’ that contains the following code.

final locator = GetIt.instance;

void setupLocator() {
locator.registerLazySingleton(() => Dio());
locator.registerLazySingleton(() => DatabaseProvider().database);
locator.registerLazySingleton(() => WeatherDao(locator.get<Future<Database>>()));
locator.registerLazySingleton(() => WeatherRepository(locator.get<Dio>(), locator.get<WeatherDao>()));
locator.registerLazySingleton(() => WeatherUseCase(locator.get<WeatherRepository>()));
locator.registerLazySingleton(() => HomeController(locator.get<WeatherUseCase>()));
}

In the ‘main.dart’ file, we will initialize the dependencies and display the ‘HomePage’ widget.

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await DatabaseProvider().initDatabase();
setupLocator();

runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Weather App',
home: HomePage(),
);
}
}

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = locator.get<HomeController>();
return Scaffold(
appBar: AppBar(
title: Text('Weather App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Weather in ${controller.cityName}',
style: TextStyle(fontSize: 32.0),
),
SizedBox(height: 16.0),
Text(
'${controller.temperature.toStringAsFixed(1)} °C',
style: TextStyle(fontSize: 64.0),
),
SizedBox(height: 16.0),
Text(
controller.description,
style: TextStyle(fontSize: 24.0),
),
SizedBox(height: 32.0),
ElevatedButton(
onPressed: () => controller.getWeather('London'),
child: Text('Get Weather'),
),
],
),
),
);
}
}

class HomeController extends GetxController {
final WeatherUseCase _useCase;

HomeController(this._useCase);

final cityName = ''.obs;
final temperature = 0.0.obs;
final description = ''.obs;

void getWeather(String city) async {
final result = await _useCase(city);
result.fold(
(failure) => print(failure.message),
(weather) {
cityName.value = weather.cityName;
temperature.value = weather.temperature;
description.value = weather.description;
},
);
}
}

As you can see, the presentation layer only depends on the ‘HomeController’ that uses the ‘WeatherUseCase’ to get the weather information. The ‘WeatherUseCase’ depends on the ‘WeatherRepository’ that uses the ‘Dio’ and ‘Sqflite’ libraries to interact with external systems. By following the Clean Architecture principles, we have separated the concerns of each layer, making the code more maintainable and testable.

Conclusion:

Clean Architecture is a powerful concept that helps developers create modular, maintainable, and testable code. By separating the concerns of each layer, we can easily replace or update one layer without affecting the others. In this article, we have shown how to use Clean Architecture in a Flutter app, with an example of a weather app that retrieves weather information from an API and stores it locally using Sqflite. By following the principles of Clean Architecture, we have created a scalable and robust app that is easy to maintain and extend.

--

--