Hello reader! Flutter has been growing popular over the years, especially in the mobile field. It's mostly easy to learn and can greatly increase speed of development. There is a posing question though for a beginner in flutter, How do I make my code clean? How do I manage state? How do I structure my code?
In the article I am going to be showing you a way to make your code clean, a structure that is flexible of course, so it can be changed or edited by you. This takes into consideration separation of concerns(which of course is a big issue).
What we are going to create
To make things extremely simple we are going to be creating an application that uses the openweathermapapi to get weather data and display some of it as text on our screen. The final application is shown below
Project Setup
This project is going to have a total of 6 folders in our lib directory
- data
- mixin
- models
- providers
- screens
- utils
For state management we are going to make use of the provider pattern with the provider plugin and dio for http calls. To use this we need to add the dependencies in our pubspec.yaml folder
Provider [https://pub.dev/packages/provider]
Dio [https://pub.dev/packages/dio]
The Process
The flow of our application is divided into different classes, each one performing a particular task.
I will just show the code and explain what the class does and at the end of this, I will explain the entire process in detail.
The Data classes
In the data folder we are going to create 2 classes weather_data
and weather_repo
The
weather_data
class is responsible for fetching the weather data from the api, and returning the data to the repo class.
ThegetWeather
method takes in adio
parameter that will be parse in by the viewhome_screen
.
When the data has been gotten successfully from the api the weather class feeds the status of the response and the actual data to theOperation
class and returns an instance of this class to theWeather Repo
Weather data
import 'package:dio/dio.dart';
import 'package:weather_art/models/country_response_model.dart';
import 'package:weather_art/utils/operation.dart';
class WeatherData{
Future<Operation> getWeather(Dio dio) async{
try{
var response = await dio.get(
'http://api.openweathermap.org/data/2.5/weather?q=lagos&appid={api_key}',
).timeout(Duration(minutes: 2), onTimeout: () async{
return Response(
data: {"message": "Connection Timed out. Please try again"},
statusCode: 408);
}).catchError((error) {
return Response(
data: {"message": "Error occurred while connecting to server"},
statusCode: 508);
});
if(response.statusCode == 508 || response.statusCode == 408){
return Operation(response.statusCode, response.data);
}else{
WeatherResponse data = WeatherResponse.fromJson(response.data);
return Operation(response.statusCode, data);
}
}catch(err){
//catch err
}
}
}
}
final countryData = WeatherData();
The weather_repo
class is what calls the data class and waits for it to return data.
Weather Repo
import 'package:dio/dio.dart';
import 'package:weather_art/data/weather_data.dart';
import 'package:weather_art/utils/operation.dart';
class _WeatherRepo{
getWeatherData(Dio dio, OperationCompleted countryDataCompleted){
countryData.getWeather(dio).then((data) => countryDataCompleted(data));
}
}
_WeatherRepo countryRepo = _WeatherRepo();
The Mixin classes
In the mixin folder we are going to create one class and call it home_helper
. This class works with the provider to change the state of our view depending on the data gotten. This makes sure that the code for changing state is separated from the view itself and reduces the logic done in the view
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weather_art/data/weather_repo.dart';
import 'package:weather_art/models/country_response_model.dart';
import 'package:weather_art/providers/home_provider.dart';
import 'package:weather_art/utils/operation.dart';
mixin HomeHelper{
BuildContext _authContext;
doGetWeather(Dio dio, BuildContext context){
_authContext = context;
Provider.of<HomeProvider>(_authContext, listen: false).updateIsLoading(true);
weatherRepo.getWeatherData(dio, _weatherDataCompleted);
}
_weatherDataCompleted(Operation operation){
if(operation.code == 408 || operation.code == 508){
//handle time out
Provider.of<HomeProvider>(_authContext, listen: false).updateIsLoading(false);
print('connection timed out');
}else{
WeatherResponse weatherResponse = operation.result;
Provider.of<HomeProvider>(_authContext, listen: false).updateWeather(weatherResponse);
Provider.of<HomeProvider>(_authContext, listen: false).updateIsLoading(false);
}
}
}
The model classes
In this folder we will create our weather model
// To parse this JSON data, do
//
// final weatherResponse = weatherResponseFromJson(jsonString);
import 'dart:convert';
WeatherResponse weatherResponseFromJson(String str) => WeatherResponse.fromJson(json.decode(str));
String weatherResponseToJson(WeatherResponse data) => json.encode(data.toJson());
class WeatherResponse {
WeatherResponse({
this.weather,
this.main,
});
List<Weather> weather;
Main main;
factory WeatherResponse.fromJson(Map<String, dynamic> json) => WeatherResponse(
coord: Coord.fromJson(json["coord"]),
weather: List<Weather>.from(json["weather"].map((x) => Weather.fromJson(x))),
main: Main.fromJson(json["main"]),
);
Map<String, dynamic> toJson() => {
"coord": coord.toJson(),
"weather": List<dynamic>.from(weather.map((x) => x.toJson())),
"main": main.toJson(),
};
}
class Main {
Main({
this.temp,
});
double temp;
factory Main.fromJson(Map<String, dynamic> json) => Main(
temp: json["temp"].toDouble(),
);
Map<String, dynamic> toJson() => {
"temp": temp,
};
}
class Weather {
Weather({
this.main,
this.description,
});
String main;
String description;
factory Weather.fromJson(Map<String, dynamic> json) => Weather(
main: json["main"],
description: json["description"],
);
Map<String, dynamic> toJson() => {
"main": main,
"description": description,
};
}
The Provider Folder
We create two files here HomeProvider
and AppProvider
Home Provider
import 'package:flutter/foundation.dart';
import 'package:weather_art/models/country_response_model.dart';
class HomeProvider extends ChangeNotifier{
bool isLoading = false;
List<WeatherResponseModel> weatherList = [];
WeatherResponseModel weatherResponse;
void updateIsLoading(bool isLoadingGotten){
isLoading = isLoadingGotten;
notifyListeners();
}
void updateWeather(WeatherResponseModel weatherResponseGotten){
weatherResponse = weatherResponseGotten;
}
}
App Provider
import 'package:dio/dio.dart';
class AppProvider{
Dio dio = Dio();
}
The Screens Folder
This is where our view is located, we create a file here and call it home_screen
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:weather_art/mixin/home_helper.dart';
import 'package:weather_art/providers/app_provider.dart';
import 'package:weather_art/providers/home_provider.dart';
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with HomeHelper{
@override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
doGetWeather(
Provider.of<AppProvider>(context, listen: false).dio,
context
);
});
}
@override
Widget build(BuildContext context) {
return Consumer(
builder: (BuildContext context, HomeProvider homeProvider, Widget child){
return Scaffold(
body: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: decideLayout(homeProvider),
),
);
}
);
}
Widget decideLayout(HomeProvider homeProvider){
if(homeProvider.isLoading){
return Center(
child: CircularProgressIndicator(),
);
}else if(homeProvider.isLoading == false && homeProvider.weatherResponse == null){
return Center(
child: Text(
'Null',
style: TextStyle(
fontSize: 14
),
),
);
}else{
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Weather in Lagos',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold
),
),
Text(
'Looks Like ${homeProvider.weatherResponse.weather[0].main}',
style: TextStyle(
fontSize: 14
),
),
Text(
'Description ${homeProvider.weatherResponse.weather[0].description}',
style: TextStyle(
fontSize: 14
),
),
Text(
'Temp ${homeProvider.weatherResponse.main.temp.toString()}',
style: TextStyle(
fontSize: 14
),
),
],
);
}
}
}
The utils folder
This folder contains the class operation. You can just copy and past this in your code. The class accepts two params. The response code and the response data. This class a typedef or callback which is used to check when an operation is completed.
import 'package:flutter/material.dart';
typedef OperationCompleted(Operation operation);
class Operation {
final dynamic _result;
final int code;
Operation(this.code, this._result);
bool get succeeded => code >= 200 && code <= 226;
dynamic get result => _result;
}
The Flow
We have seen all the code and classes involved in this pattern, with some explanation on each, now let me join everything together and paint one picture by following a user's interaction with the application and telling you what happens at each juncture.
A user is clicking around his phone looking for what to do then he says "what's the weather gonna be like today", he then proceeds to open our app. The user is presented with our home_screen
, our home_screen
uses a mixin which we called HomeHelper
and also uses the HomeProvider
to manage it's own state.
The isLoading
in the HomeProvider
class is false so the user sees a circular progress indicating that something is loading. While this is happening the initState
calls the doGetWeather
with the needed parameters. The doGetWeather
is from mixin HomeHelper
. The doGetWeather
calls the weatherRepo.getWeatherData
which is from the _WeatherRepo
class. This calls the getWeather
in the WeatherData
class which is a Future
so this method expects something to be returned from getWeather
. getWeather
is responsible for getting the data from the api and returns Operation
. Operation
accepts two parameters which are the status code and the data from the api. When the operation is return from the getWeather
, the data is then passed backwards.
We should recall that the repo class still expects some data, when the operation class is returned the .then
in the repo class is called and data is passed to the countryDataCompleted
, this in turn triggers the _weatherDataCompleted
in HomeHelper
. This is where we change our UI and update our data using methods we have created in the HomeProvider.
Conclusion
Using this architecture we can see that we have succeeded in separating the UI from our logic quite well. Everything is separated making the code easy to read and edit.
Top comments (1)
Great to see you're spending time teaching people your skills now. Nice read! 👌🏾