DEV Community

Cover image for Flutter Architecture
Okocha Ebube
Okocha Ebube

Posted on • Updated on

Flutter Architecture

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

Alt Text

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.
The getWeather method takes in a dio parameter that will be parse in by the view home_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 the Operation class and returns an instance of this class to the Weather 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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);

    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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,

  };
}

Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

App Provider

import 'package:dio/dio.dart';

class AppProvider{
  Dio dio = Dio();

}
Enter fullscreen mode Exit fullscreen mode

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
            ),
          ),
        ],
      );
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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 _WeatherRepoclass. 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.

Source Code

[https://github.com/Marcusjnr/weather]

Top comments (1)

Collapse
 
lordtx profile image
Johnpaul "Mark Jordan"

Great to see you're spending time teaching people your skills now. Nice read! πŸ‘ŒπŸΎ