visit
Hello everyone. In the previous article, we spoke about clean architecture concepts. Now it's time to implement them.
flutter create sunFlare
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.dart (app initialization) and home.dart (main view of application). Code of these files you can see below:
import 'package:flutter/material.dart';
import 'package:sunFlare/application/app.dart';
void main() {
runApp(Application());
}
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(),
);
}
}
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();
}
}
As you can understand from the previous article, 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.
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,
});
}
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,
});
}
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,
});
}
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);
}
}
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'];
}
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 = '//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();
}
}
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);
}
}
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();
}
}
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.
flutter:
...
mobx: any
flutter_mobx: any
Also, we need to add packets to dev dependencies to generate files that allow us to use annotations @observable, @computed, @action. Just a bit of syntax sugar.
dev_dependencies:
...
mobx_codegen: any
build_runner: any
We already have the view — Home. Just add the file called home_state.dart
nearby. This file will contain viewModel (which in flutter is usually called state for some reason). And add the code to the file:
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 isLoading. solarActivities
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.
To generate class home_state.g.dart
(to use @obsevable annotations), just call the command in terminal:
flutter packages pub run build_runner build
Let’s come back to our view — home.dart
and update it.
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()));
}
}
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 app.dart and throw HomeModule.homeState() to Home constructor:
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()),
);
}
}
Full code example you can find at .