In this part I will explain the rest of directory structure of Flutter Clean Architecture. If you haven’t read the first part, click here and to read it.
Directory Structure (Part 2)
Feature
In clean architecture, we divide our application into features. For example, in this project will have a weather feature. Each feature will have its own (but not always neccessarily) domain
, data
and presentation
.
Data
data
as it’s namesake, will deals with all data needed by the app. It contains (not limited to) model
, repository
implementation, and data sources
. Model
and repository
classes are self-explanatory, and data sources
will be used to access data from both local and remote sources.
Let’s create a model for WeatherAPI current weather response.
// lib/features/weather/data/models/current_weather_model.dart
import 'package:clean_architecture/core/data/remote/model/weather_api_response_model.dart';
import 'package:json_annotation/json_annotation.dart';
part 'current_weather_model.g.dart';
@JsonSerializable()
class CurrentWeatherModel extends WeatherApiResponseModel {
@JsonKey(name: 'current')
final WeatherApiDataModel? data;
CurrentWeatherModel({
required this.data,
required super.location,
required super.error,
});
factory CurrentWeatherModel.fromJson(Map<String, dynamic> json) =>
_$CurrentWeatherModelFromJson(json);
}
@JsonSerializable(fieldRename: FieldRename.snake)
class WeatherApiDataModel {
final DateTime lastUpdated;
final double tempC;
final double feelslikeC;
final WeatherApiConditionModel condition;
final double windKph;
final String windDir;
final double precipMm;
final int humidity;
final int cloud;
final double visKm;
final double uv;
const WeatherApiDataModel({
required this.lastUpdated,
required this.tempC,
required this.feelslikeC,
required this.condition,
required this.windKph,
required this.windDir,
required this.precipMm,
required this.humidity,
required this.cloud,
required this.visKm,
required this.uv,
});
factory WeatherApiDataModel.fromJson(Map<String, dynamic> json) =>
_$WeatherApiDataModelFromJson(json);
}
@JsonSerializable()
class WeatherApiConditionModel {
final String text;
final String icon;
const WeatherApiConditionModel({
required this.text,
required this.icon,
});
factory WeatherApiConditionModel.fromJson(Map<String, dynamic> json) =>
_$WeatherApiConditionModelFromJson(json);
}
The next part is create the data_source
, which will be responsible to access data from both local and remote sources. For accessing WeatherAPI, network
will be used.
// lib/features/weather/data/data_sources/remote/weather_api_remote_data_source.dart
import 'dart:convert';
import 'package:clean_architecture/core/data/remote/hosts.dart';
import 'package:clean_architecture/core/network/network.dart';
import 'package:clean_architecture/features/weather/data/models/current_weather_model.dart';
abstract class WeatherApiRemoteDataSource {
Future<CurrentWeatherModel> getCurrentWeather(String city);
}
class WeatherApiRemoteDataSourceImpl implements WeatherApiRemoteDataSource {
final Network network;
WeatherApiRemoteDataSourceImpl({required this.network});
@override
Future<CurrentWeatherModel> getCurrentWeather(String city) async {
final uri = Uri(
scheme: 'https',
host: weatherApiHost,
path: 'v1/current.json',
queryParameters: {
'key': 'KEY',
'q': city,
},
);
final response = await network.get(uri);
final jsonResponse = jsonDecode(response) as Map<String, dynamic>;
return CurrentWeatherModel.fromJson(jsonResponse);
}
}
Domain
domain
stores entities
, use cases
and abstract repository
classes, as they are the ‘domain’ or ‘subject’ area of an application. If you aren’t familiar with the term, you can think that this ‘domain’ is the base requirement of an application.
First thing, we need to create an entity
for current weather data. This entity will represent what kind of data we want to show to the user.
// lib/features/weather/domain/entities/current_weather.dart
import 'package:clean_architecture/features/weather/data/models/current_weather_model.dart';
class CurrentWeather {
final DateTime? lastUpdated;
final double? tempC;
final double? feelslikeC;
final double? windKph;
final String? windDir;
final double? precipMm;
final int? humidity;
final int? cloud;
final double? visKm;
final double? uv;
final String? conditionText;
final String? conditionIcon;
final String? locationName;
final String? locationRegion;
final String? locationCountry;
const CurrentWeather({
this.lastUpdated,
this.tempC,
this.feelslikeC,
this.windKph,
this.windDir,
this.precipMm,
this.humidity,
this.cloud,
this.visKm,
this.uv,
this.conditionText,
this.conditionIcon,
this.locationName,
this.locationRegion,
this.locationCountry,
});
factory CurrentWeather.fromModel(CurrentWeatherModel model) => CurrentWeather(
lastUpdated: model.data?.lastUpdated,
tempC: model.data?.tempC,
feelslikeC: model.data?.feelslikeC,
windKph: model.data?.windKph,
windDir: model.data?.windDir,
precipMm: model.data?.precipMm,
humidity: model.data?.humidity,
cloud: model.data?.cloud,
visKm: model.data?.visKm,
uv: model.data?.uv,
conditionText: model.data?.condition.text,
conditionIcon: model.data?.condition.icon,
locationName: model.location?.name,
locationRegion: model.location?.region,
locationCountry: model.location?.country,
);
}
Then create an abstract class for WeatherAPI repository.
// features/weather/domain/repositories/weather_api_repository.dart
import 'package:clean_architecture/core/error/failure.dart';
import 'package:clean_architecture/features/weather/domain/entities/current_weather.dart';
import 'package:fpdart/fpdart.dart';
abstract class WeatherApiRepository {
Future<Either<Failure, CurrentWeather>> getCurrentWeather(String city);
}
Things to keep in mind: data source
returns model
, repository
uses one or more data source
and gathers data from them, process it and returns entity
. With this pattern, you can create an entity
that contains data from several sources.
Next we will create a use case
for getting current weather data using the repository above.
// lib/features/weather/domain/use_cases/get_current_weather.dart
import 'package:clean_architecture/core/domain/use_case.dart';
import 'package:clean_architecture/core/error/failure.dart';
import 'package:clean_architecture/features/weather/domain/entities/current_weather.dart';
import 'package:clean_architecture/features/weather/domain/repositories/weather_api_repository.dart';
import 'package:equatable/equatable.dart';
import 'package:fpdart/fpdart.dart';
class GetCurrentWeather
extends UseCase<CurrentWeather, GetCurrentWeatherParams> {
final WeatherApiRepository weatherApiRepository;
GetCurrentWeather({required this.weatherApiRepository});
@override
Future<Either<Failure, CurrentWeather>> execute(
GetCurrentWeatherParams params,
) async {
return weatherApiRepository.getCurrentWeather(params.city);
}
}
class GetCurrentWeatherParams extends Equatable {
final String city;
const GetCurrentWeatherParams({required this.city});
@override
List<Object?> get props => [city];
}
We almost finished the data
and domain
for weather feature. Last thing is to create an implementation of weather_api_repository
in the data
layer.
// lib/features/weather/data/repositories/weather_api_repository_impl.dart
import 'package:clean_architecture/core/error/failure.dart';
import 'package:clean_architecture/core/error/server_failure.dart';
import 'package:clean_architecture/features/weather/data/data_sources/remote/weather_api_remote_data_source.dart';
import 'package:clean_architecture/features/weather/domain/entities/current_weather.dart';
import 'package:clean_architecture/features/weather/domain/repositories/weather_api_repository.dart';
import 'package:fpdart/fpdart.dart';
class WeatherApiRepositoryImpl implements WeatherApiRepository {
final WeatherApiRemoteDataSource weatherApiRemoteSource;
WeatherApiRepositoryImpl({required this.weatherApiRemoteSource});
@override
Future<Either<Failure, CurrentWeather>> getCurrentWeather(String city) async {
try {
final result = await weatherApiRemoteSource.getCurrentWeather(city);
if (result.error != null) {
return left(ServerFailure(message: result.error!.message));
}
final entity = CurrentWeather.fromModel(result);
return right(entity);
} on Exception catch (e) {
return left(ServerFailure(message: e.toString(), cause: e));
}
}
}
Presentation
presentation
stores pages and widgets. These are the ‘presentation’ or ‘view’ area of the application. If you aren’t familiar with the term, you can think that this ‘presentation’ is the actual view of an application.
In this presentation
layer, we use auto_route package to manage our pages routing. Then flutter_bloc package will help us to manage state management hence keeping our code clean because we will separate the logic from the UI.
When creating a page/UI, keep in mind that it should be dumb. Means that it should not contain any logic. The logic should be handled in the bloc
. Generally, a bloc is composed of state, event, and the bloc itself. The state will be used to manage the state of the page and the event will be used to communicate with the bloc to update the state. Let’s create the bloc
for the current_weather
page.
// lib/features/current_weather/presentation/bloc/current_weather_states.dart
part of 'current_weather_bloc.dart';
abstract class CurrentWeatherState extends Equatable {
const CurrentWeatherState();
}
class CurrentWeatherInitialState extends CurrentWeatherState {
const CurrentWeatherInitialState();
@override
List<Object?> get props => [];
}
class CurrentWeatherLoadingState extends CurrentWeatherState {
const CurrentWeatherLoadingState();
@override
List<Object?> get props => [];
}
class CurrentWeatherLoadedState extends CurrentWeatherState {
final CurrentWeather currentWeather;
const CurrentWeatherLoadedState({required this.currentWeather});
@override
List<Object?> get props => [currentWeather];
}
class CurrentWeatherErrorState extends CurrentWeatherState
implements ErrorState {
@override
final String message;
@override
final Exception? cause;
const CurrentWeatherErrorState({required this.message, this.cause});
@override
List<Object?> get props => [message, cause];
}
// lib/features/current_weather/presentation/bloc/// features/current_weather/presentation/bloc/current_weather_events.dart
part of 'current_weather_bloc.dart';
abstract class CurrentWeatherEvent extends Equatable {
const CurrentWeatherEvent();
}
class GetCurrentWeatherEvent extends CurrentWeatherEvent {
final String city;
const GetCurrentWeatherEvent({required this.city});
@override
List<Object?> get props => [city];
}
// lib/features/current_weather/presentation/bloc/current_weather_bloc.dart
import 'package:clean_architecture/core/presentation/bloc/error_state.dart';
import 'package:clean_architecture/features/weather/domain/entities/current_weather.dart';
import 'package:clean_architecture/features/weather/domain/use_cases/get_current_weather.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
part 'current_weather_events.dart';
part 'current_weather_states.dart';
class CurrentWeatherBloc
extends Bloc<CurrentWeatherEvent, CurrentWeatherState> {
final GetCurrentWeather getCurrentWeather;
CurrentWeatherBloc({
required this.getCurrentWeather,
}) : super(const CurrentWeatherInitialState()) {
on<GetCurrentWeatherEvent>(_onGetCurrentWeatherEvent);
}
Future<void> _onGetCurrentWeatherEvent(
GetCurrentWeatherEvent event,
Emitter<CurrentWeatherState> emit,
) async {
emit(const CurrentWeatherLoadingState());
final result = await getCurrentWeather.execute(
GetCurrentWeatherParams(city: event.city),
);
result.fold(
(l) => emit(CurrentWeatherErrorState(message: l.message)),
(r) => emit(CurrentWeatherLoadedState(currentWeather: r)),
);
}
}
We are missing the ErrorState
class, let’s create it in the core so all error states can be inherited from it and makes all error uniform across the application.
// lib/core/presentation/bloc/error_state.dart
abstract class ErrorState {
final String message;
final Exception? cause;
const ErrorState({
required this.message,
this.cause,
});
}
That’s it. Now we have the bloc
for the current_weather
page. The code itself is quite self-explanatory. When the CurrentWeatherBloc
receives a GetCurrentWeatherEvent
event, it will emit a CurrentWeatherLoadingState
and then a CurrentWeatherLoadedState
or CurrentWeatherErrorState
depending on the result of the GetCurrentWeather
use case.
The next part is to create weather page in presentation
directory.
// lib/features/current_weather/presentation/current_weather_page.dart
import 'package:auto_route/auto_route.dart';
import 'package:clean_architecture/features/weather/presentation/bloc/current_weather_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@RoutePage()
class CurrentWeatherPage extends StatefulWidget {
const CurrentWeatherPage({super.key});
@override
State<CurrentWeatherPage> createState() => _CurrentWeatherPageState();
}
class _CurrentWeatherPageState extends State<CurrentWeatherPage> {
final _cityTextCtl = TextEditingController();
final _cityTextFocus = FocusNode();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Current Weather'),
),
body: BlocBuilder<CurrentWeatherBloc, CurrentWeatherState>(
builder: (context, state) {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
TextField(
controller: _cityTextCtl,
focusNode: _cityTextFocus,
decoration: const InputDecoration(
hintText: 'City',
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () {
context.read<CurrentWeatherBloc>().add(
GetCurrentWeatherEvent(
city: _cityTextCtl.text,
),
);
},
child: const Text('Get Weather'),
),
const SizedBox(height: 16),
_buildWeather(state),
],
);
},
),
);
}
Widget _buildWeather(CurrentWeatherState state) {
if (state is CurrentWeatherLoadedState) {
_cityTextFocus.unfocus();
final weatherIconUrl =
'https:${state.currentWeather.conditionIcon ?? '//placehold.co/64x64/png'}';
return Column(
children: [
Image.network(weatherIconUrl),
Text(
state.currentWeather.conditionText ?? '-',
style: Theme.of(context).textTheme.headlineSmall,
),
Text(
'${state.currentWeather.locationName}, ${state.currentWeather.locationRegion}'),
Text('${state.currentWeather.locationCountry}'),
const SizedBox(height: 16),
GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildDataCard(
'Temp (C)',
'${state.currentWeather.tempC ?? '-'}',
),
_buildDataCard(
'Feels Like (C)',
'${state.currentWeather.feelslikeC ?? '-'}',
),
_buildDataCard(
'Wind (km/h)',
'${state.currentWeather.windKph ?? '-'}',
),
_buildDataCard(
'Wind Dir',
state.currentWeather.windDir,
),
_buildDataCard(
'Precip (mm)',
'${state.currentWeather.precipMm ?? '-'}',
),
_buildDataCard(
'Humidity (%)',
'${state.currentWeather.humidity ?? '-'}',
),
_buildDataCard(
'Cloud (%)',
'${state.currentWeather.cloud ?? '-'}',
),
_buildDataCard(
'Vis (km)',
'${state.currentWeather.visKm ?? '-'}',
),
_buildDataCard(
'UV',
'${state.currentWeather.uv ?? '-'}',
),
],
),
const SizedBox(height: 16),
Text(
'Last Updated: ${state.currentWeather.lastUpdated}',
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
if (state is CurrentWeatherLoadingState) {
return const Center(child: CircularProgressIndicator());
}
if (state is CurrentWeatherErrorState) {
return Text(state.message);
}
return const SizedBox();
}
Widget _buildDataCard(String header, String? content) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(header, textAlign: TextAlign.center),
Text(
content ?? '-',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineLarge,
),
],
),
);
}
}
As for the routing I mentioned in the previously, we will create an app_router.dart
file in the core/router
directory.
// lib/core/router/app_router.dart
import 'package:auto_route/auto_route.dart';
import 'package:clean_architecture/core/router/app_router.gr.dart';
@AutoRouterConfig()
class AppRouter extends RootStackRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(
page: CurrentWeatherRoute.page,
initial: true,
),
];
}
See you in the last part!
I think it’s already a long post for this part. We’ll continue in the next part to complete our application, including dependency injection
and testing
.
If there are any missing steps or typos, please let me know, or you can always open an issue or directly create a pull request on this blog repository.