Hello everyone. In , we spoke about clean architecture concepts. Now it's time to implement them. the previous article For a good understanding, we are going to develop a simple app. The app receives info about solar flares and storms from the NASA API and presents it on the view. Create Project First of all, you have to install flutter. You may check the installation process. here After that let’s create a project. There are few ways to do it. The basic way is to create a project by terminal command: flutter create sunFlare Now we have an example of a project. Let's modify it a bit by removing unwanted code and setting up directories. So, we’ve created directories for each layer (data, domain, and presentation) and another one for the application layer which will contain application initialization and dependency injections. Also, we’ve created files (app initialization) and (main view of application). Code of these files you can see below: app.dart home.dart main: import 'package:flutter/material.dart'; import 'package:sunFlare/application/app.dart'; void main() { runApp(Application()); } app: import 'package:flutter/material.dart'; import 'package:sunFlare/presentation/home.dart'; class Application extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: Home(), ); } } home: import 'package:flutter/material.dart'; class Home extends StatefulWidget { @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> { @override Widget build(BuildContext context) { return Scaffold(); } } The first step behind and now it’s time to develop a domain. Domain As you can understand from , the domain layer is the most important part of the application and it’s the first layer you should design. By the way, if you design a backend system, you just think about what entities (aggregators) should exist in the system and design it. So far as we are designing the client application, we already have some initial data (which we fetch from the backend), so we should take it into consideration when designing our domain entities. the previous article However, it doesn’t mean we have to use the same data format as we receive from the backend. Our application has its own business logic, so we have to define entities that participate in this process. Now then, here is our domain-level models: import 'package:meta/meta.dart'; class GeoStorm { final String gstId; final DateTime startTime; GeoStorm({ @required this.gstId, @required this.startTime, }); } import 'package:meta/meta.dart'; class SolarFlare { final String flrID; final DateTime startTime; final DateTime endTime; final String classType; final String sourceLocation; SolarFlare({ @required this.flrID, @required this.startTime, this.endTime, @required this.classType, @required this.sourceLocation, }); } We are going to implement a use case for collecting the last solar activities (geo storm and solar flare), so let’s define the model first. import 'package:meta/meta.dart'; import 'solar_flare.dart'; import 'geo_storm.dart'; class SolarActivities { final SolarFlare lastFlare; final GeoStorm lastStorm; SolarActivities({ @required this.lastFlare, @required this.lastStorm, }); } Fine. Now we have business-level models. Let’s define protocols for repositories returning these models. import 'package:meta/meta.dart'; import 'package:sunFlare/domain/entities/geo_storm.dart'; abstract class GeoStormRepo { Future<List<GeoStorm>> getStorms({ @required DateTime from, @required DateTime to, }); import 'package:meta/meta.dart'; import 'package:sunFlare/domain/entities/solar_flare.dart'; abstract class SolarFlareRepo { Future<List<SolarFlare>> getFlares({ @required DateTime from, @required DateTime to, }); } And as I’ve promised here is a use case. import 'package:sunFlare/domain/entities/solar_activities.dart'; import 'package:sunFlare/domain/repos/geo_storm_repo.dart'; import 'package:sunFlare/domain/repos/solar_flare_repo.dart'; class SolarActivitiesUseCase { final GeoStormRepo _geoStormRepo; final SolarFlareRepo _solarFlareRepo; SolarActivitiesUseCase(this._geoStormRepo, this._solarFlareRepo); Future<SolarActivities> getLastSolarActivities() async { final fromDate = DateTime.now().subtract(Duration(days: 365)); final toDate = DateTime.now(); final storms = await _geoStormRepo.getStorms(from: fromDate, to: toDate); final flares = await _solarFlareRepo.getFlares(from: fromDate, to: toDate); return SolarActivities(lastFlare: flares.last, lastStorm: storms.last); } } Good. Now let me clarify what we’ve done just now. First of all, we designed the data models we needed for our use case. After that, we found out where to get those models from and defined repository protocols. Finally, we implemented a direct use case, which function is to return the last solar activities. It calls functions of repositories, extracts and collects last solar activities, and returns them. The tree of domain layer directory should looks like this: We’ve just implemented the core of our application — the business logic. Now it's time to take care of the data layer. Data The first step is quite similar to the first step of the previous section - we are going to design the data models which will be fetched from the network. import 'package:sunFlare/domain/entities/geo_storm.dart'; class GeoStormDTO { final String gstId; final DateTime startTime; final String link; GeoStormDTO.fromApi(Map<String, dynamic> map) : gstId = map['gstID'], startTime = DateTime.parse(map['startTime']), link = map['link']; } import 'package:sunFlare/domain/entities/solar_flare.dart'; class SolarFlareDTO { final String flrID; final DateTime startTime; final DateTime endTime; final String classType; final String sourceLocation; final String link; SolarFlareDTO.fromApi(Map<String, dynamic> map) : flrID = map['flrID'], startTime = DateTime.parse(map['beginTime']), endTime = map['endTime'] != null ? DateTime.parse(map['endTime']) : null, classType = map['classType'], sourceLocation = map['sourceLocation'], link = map['link']; } DTO means Data Transfer Object. It’s a usual name for transport layer models. The models implement constructors parsing JSON. The next code snippet contains the implementation of NasaService, which is responsible for NASA API requests. import 'package:dio/dio.dart'; import 'package:sunFlare/data/entities/geo_storm_dto.dart'; import 'package:sunFlare/data/entities/solar_flare_dto.dart'; import 'package:intl/intl.dart'; class NasaService { static const _BASE_URL = 'https://kauai.ccmc.gsfc.nasa.gov'; final Dio _dio = Dio( BaseOptions(baseUrl: _BASE_URL), ); Future<List<GeoStormDTO>> getGeoStorms(DateTime from, DateTime to) async { final response = await _dio.get( '/DONKI/WS/get/GST', queryParameters: { 'startDate': DateFormat('yyyy-MM-dd').format(from), 'endDate': DateFormat('yyyy-MM-dd').format(to) }, ); return (response.data as List).map((i) => GeoStormDTO.fromApi(i)).toList(); } Future<List<SolarFlareDTO>> getFlares(DateTime from, DateTime to) async { final response = await _dio.get( '/DONKI/WS/get/FLR', queryParameters: { 'startDate': DateFormat('yyyy-MM-dd').format(from), 'endDate': DateFormat('yyyy-MM-dd').format(to) }, ); return (response.data as List) .map((i) => SolarFlareDTO.fromApi(i)) .toList(); } } The service contains methods calling API and returning DTO objects. Now we have to extend our DTO models. We are going to implement mappers from data layer models to domain layer models. import 'package:sunFlare/domain/entities/geo_storm.dart'; class GeoStormDTO { final String gstId; final DateTime startTime; final String link; GeoStormDTO.fromApi(Map<String, dynamic> map) : gstId = map['gstID'], startTime = DateTime.parse(map['startTime']), link = map['link']; } extension GeoStormMapper on GeoStormDTO { GeoStorm toModel() { return GeoStorm(gstId: gstId, startTime: startTime); } } import 'package:sunFlare/domain/entities/solar_flare.dart'; class SolarFlareDTO { final String flrID; final DateTime startTime; final DateTime endTime; final String classType; final String sourceLocation; final String link; SolarFlareDTO.fromApi(Map<String, dynamic> map) : flrID = map['flrID'], startTime = DateTime.parse(map['beginTime']), endTime = map['endTime'] != null ? DateTime.parse(map['endTime']) : null, classType = map['classType'], sourceLocation = map['sourceLocation'], link = map['link']; } extension SolarFlareMapper on SolarFlareDTO { SolarFlare toModel() { return SolarFlare( flrID: flrID, startTime: startTime, classType: classType, sourceLocation: sourceLocation); } } And finally it’s time to implement repositories which protocols are in the domain layer. import 'package:sunFlare/data/services/nasa_service.dart'; import 'package:sunFlare/domain/entities/geo_storm.dart'; import 'package:sunFlare/domain/repos/geo_storm_repo.dart'; import 'package:sunFlare/data/entities/geo_storm_dto.dart'; class GeoStormRepoImpl extends GeoStormRepo { final NasaService _nasaService; GeoStormRepoImpl(this._nasaService); @override Future<List<GeoStorm>> getStorms({DateTime from, DateTime to}) async { final res = await _nasaService.getGeoStorms(from, to); return res.map((e) => e.toModel()).toList(); } } import 'package:sunFlare/data/services/nasa_service.dart'; import 'package:sunFlare/domain/entities/solar_flare.dart'; import 'package:sunFlare/domain/repos/solar_flare_repo.dart'; import 'package:sunFlare/data/entities/solar_flare_dto.dart'; class SolarFlareRepoImpl extends SolarFlareRepo { final NasaService _nasaService; SolarFlareRepoImpl(this._nasaService); @override Future<List<SolarFlare>> getFlares({DateTime from, DateTime to}) async { final res = await _nasaService.getFlares(from, to); return res.map((e) => e.toModel()).toList(); } } The constructors of repositories accept NasaService. Methods call requests, map DTO models to domain models by mappers we have just realized, and return domain models to the domain layer. This is how the Data directory should look like now. User Interface I am not going to write a lot about presentation layer architectures in this article. There is a range of variants and if you are interested in this topic, please let me know in the comments. Almost there. Domain and Data are behind, now it’s time for Presentation Layer. As long as we decided to use the MVVM pattern for presentation layer architecture, let’s add dependencies for RX. flutter: ... mobx: any flutter_mobx: any Also, we need to add packets to dev dependencies to generate files that allow us to use annotations , , . Just a bit of syntax sugar. @observable @computed @action dev_dependencies: ... mobx_codegen: any build_runner: any We already have the view — Home. Just add the file called nearby. This file will contain viewModel (which in flutter is usually called state for some reason). And add the code to the file: home_state.dart import 'package:mobx/mobx.dart'; import 'package:sunFlare/domain/use_cases/solar_activities_use_case.dart'; import 'package:sunFlare/domain/entities/solar_activities.dart'; part 'home_state.g.dart'; class HomeState = HomeStateBase with _$HomeState; abstract class HomeStateBase with Store { HomeStateBase(this._useCase) { getSolarActivities(); } final SolarActivitiesUseCase _useCase; @observable SolarActivities solarActivities; @observable bool isLoading = false; @action Future<void> getSolarActivities() async { isLoading = true; solarActivities = await _useCase.getLastSolarActivities(); isLoading = false; } } Nothing special here. We call our use case in the constructor. Also, we have two observable properties — solarActivities and is just the model returned by the use case. isLoading shows us if the request is in progress. The view will subscribe to these variables soon. isLoading. solarActivities To generate class (to use annotations), just call the command in terminal: home_state.g.dart @obsevable flutter packages pub run build_runner build Let’s come back to our view — and update it. home.dart import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:sunFlare/presentation/home_state.dart'; class Home extends StatefulWidget { HomeState homeState; Home({Key key, @required this.homeState}) : super(key: key); @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> { Widget _body() { return Observer( builder: (_) { if (widget.homeState.isLoading) return Center( child: CircularProgressIndicator(), ); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Last Solar Flare Date: ${widget.homeState.solarActivities.lastFlare.startTime}'), Text( 'Last Geo Storm Date: ${widget.homeState.solarActivities.lastStorm.startTime}'), ], ); }, ); } @override Widget build(BuildContext context) { return Scaffold(body: SafeArea(child: _body())); } } HomeState object in initializator as an argument and extremely simple UI. If isLoading == true, we show an activity indicator. If not, we present data of the last solar activities. Application Finish him! We have everything we need, now it's time to keep it together. The application layer includes dependency injections and initializations. Create directory dependencies in the application directory and add two files there. import 'package:sunFlare/domain/repos/geo_storm_repo.dart'; import 'package:sunFlare/domain/repos/solar_flare_repo.dart'; import 'package:sunFlare/data/repos/geo_storm_repo.dart'; import 'package:sunFlare/data/repos/solar_flare_repo.dart'; import 'package:sunFlare/data/services/nasa_service.dart'; class RepoModule { static GeoStormRepo _geoStormRepo; static SolarFlareRepo _solarFlareRepo; static NasaService _nasaService = NasaService(); static GeoStormRepo geoStormRepo() { if (_geoStormRepo == null) { _geoStormRepo = GeoStormRepoImpl(_nasaService); } return _geoStormRepo; } static SolarFlareRepo solarFlareRepo() { if (_solarFlareRepo == null) { _solarFlareRepo = SolarFlareRepoImpl(_nasaService); } return _solarFlareRepo; } } import 'package:sunFlare/domain/use_cases/solar_activities_use_case.dart'; import 'package:sunFlare/presentation/home_state.dart'; import 'repo_module.dart'; class HomeModule { static HomeState homeState() { return HomeState(SolarActivitiesUseCase( RepoModule.geoStormRepo(), RepoModule.solarFlareRepo())); } } Come back to and throw to Home constructor: app.dart HomeModule.homeState() import 'package:flutter/material.dart'; import 'package:sunFlare/application/dependencies/home_module.dart'; import 'package:sunFlare/presentation/home.dart'; class Application extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: Home(homeState: HomeModule.homeState()), ); } } Run the application and enjoy the result :) Congratulations! We’ve got it. Now you understand how to build the clean architecture of the flutter application. In case you don’t understand something, feel free to ask me in the comments. Full code example you can find at . GitHub