Flutter Weather App

Flutter

+

Get-X

4 Janurary 2023

Github Repository

A guide on how to build a flutter weather app for mobile utilizing location services. In this article we will be using flutter and Get-X for state management.

Get-X implements uses the MVC approach:

Model - View - Controller

This method is used to separate user interface from logic as much as possible to make code easier to understand.

We will be using the OpenWeather API to get access to current weather and forecast.

Here we have access to every weather event on Earth.

1. Let's Get Started

I am using Android Studio Dolphin on Windows 10. My device used will be a of android type. An IOS device is out of scope in this tutorial.

File => New => NewFlutterProject

Type flutter_weather_app and click Finish.

Let's see everything is running correctly. Select an android mobile device of your choice and click on the Run button. Shown below is the default view.

2. Add Dependencies

Modify your pubspec.yamlto look like the following:

pubspec.yaml

1 2 3 4 5 6 7 8 9 10 dependencies: flutter: sdk: flutter get: ^4.6.5 material_design_icons_flutter: ^6.0.7096 sizer: ^2.0.15 date_time_format: ^2.0.1 geolocator: ^9.0.2 http: ^0.13.5 google_fonts: ^3.0.1

pubspec.yaml

1 2 3 assets: - assets/images/ - assets/weather/

And now click on the pubget in the flutter commands toolbar.

Please ensure the compileSdkVersion 33 is the compile version in the android/app/build.gradle file.

3. Add Directories & Files

Modify the lib directory to look like the following:

Under assets/images we need to add some images. You can find the all these files on my github repository.

When your app is used from your pc the default address will be Mountain View, United States where Google is based so we need to add an image mountainview.jpg under assets/images.

If you use the app from a physical mobile device it will be best to load an image based on where you are. This is where you can customize the view to suit your area. The getImage() function in home_controller.dart will need to be modified to suit this.

My area is Lower Hutt so I have a added an image lower_hutt.jpg and added the following lines 6-8 to the getImage() function:

part of home_controller.dart

1 2 3 4 5 6 7 8 9 10 11 12 13 getImage(String? name) { if (name == 'Mountain View') { locationImage.value = 'assets/images/mountainview.jpg'; return 'assets/images/mountainview.jpg'; } if (name == 'Lower Hutt City') { locationImage.value = 'assets/images/lower_hutt.jpg'; return 'assets/images/lower_hutt.jpg'; } else { locationImage.value = 'assets/images/no-image.png'; return 'assets/images/no-image.png';} }

4. Permission

On Android phones we need to add this statement into the AndroidManifest.xml file:<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

5. Constants

The constants file will house all the variables that will not change during the lifecycle of our app:

strings.dart

1 2 3 const String apiKey = "yourApikey"; const String currentEndpoint = "https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API key}"; const String daysEndpoint = "api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&appid={API key}";

Use your OpenWeather key to replace "yourApiKey".

6. Services

Now we need to build the function that will access the Open Weather API.

The newtork call made will by using the http package.

The function will have a declared variable res, short for response.

Next we will have an if statement to see if we have a successful status 200 code to enable us to write the data to a new variable and return this variable.

api_services.dart

1 2 3 4 5 6 7 8 9 10 11 12 13 import 'package:getx_weather_app/models/current_model.dart'; import 'package:http/http.dart' as http; import '../constants/strings.dart'; Future getCurrentWeather(lat, long) async { var link = "https://api.openweathermap.org/data/2.5/weather?lat=$lat&lon=$long&appid=$apiKey&units=metric"; var res = await http.get(Uri.parse(link)); if (res.statusCode == 200) { var data = currentWeatherFromJson(res.body.toString()); return data; } }

7. Models

Models in Flutter are way do define different data types and the way it interacts with each other.

Here is an example of current weather API response showing some data from Open Weather:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 { "coord": { "lon": 10.99, "lat": 44.34 }, "weather": [ { "id": 501, "main": "Rain", "description": "moderate rain", "icon": "10d" } ], "base": "stations", "main": { "temp": 298.48, "feels_like": 298.74, "temp_min": 297.56, "temp_max": 300.05, "pressure": 1015, "humidity": 64, "sea_level": 1015, "grnd_level": 933 }, "visibility": 10000, "wind": { "speed": 0.62, "deg": 349, "gust": 1.18 }, "rain": { "1h": 3.16 }, "clouds": { "all": 100 }, "dt": 1661870592, "sys": { "type": 2, "id": 2075663, "country": "IT", "sunrise": 1661834187, "sunset": 1661882248 }, "timezone": 7200, "id": 3163858, "name": "Zocca", "cod": 200 }

This API response shows how information is grouped together, this will help us define our models, and which information we choose to select what we show in our app.

We will have 4 models to represent our data: CurrentWeather, Weather, Main, Wind.

current_model.dart

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import 'dart:convert'; import 'package:getx_weather_app/models/main_model.dart'; import 'package:getx_weather_app/models/weather_model.dart'; import 'package:getx_weather_app/models/wind_model.dart'; CurrentWeather currentWeatherFromJson(String str) => CurrentWeather.fromJson(json.decode(str)); class CurrentWeather { CurrentWeather({ this.weather, this.main, this.wind, this.name, }); List<Weather>? weather; Main? main; Wind? wind; int? dt; String? name; factory CurrentWeather.fromJson(Map<String, dynamic> json) => CurrentWeather( weather: List<Weather>.from(json["weather"].map((x) => Weather.fromJson(x))), main: Main.fromJson(json["main"]), wind: Wind.fromJson(json["wind"]), name: json["name"], ); }

weather_model.dart

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Weather { int? id; String? description; String? icon; Weather({ this.id, this.description, this.icon, }); factory Weather.fromJson(Map<String, dynamic> json) => Weather( id: json["id"], description: json["description"], icon: json["icon"], ); }

main_model.dart

1 2 3 4 5 6 7 8 9 10 11 class Main { Main({ this.temp, }); int? temp; factory Main.fromJson(Map<String, dynamic> json) => Main( temp: json["temp"].toInt(), ); }

wind_model.dart

1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Wind { Wind({ this.speed, this.deg, }); double? speed; int? deg; factory Wind.fromJson(Map<String, dynamic> json) => Wind( speed: json["speed"].toDouble(), deg: json["deg"], ); }

8. Controllers

Now its time to define our controller, this is where we bring in all the business logic to operate behind the scenes so our view can easily access it. We firstly define the GetxController class as ourHomeController.

We will also be creating functions that will accessible to all our app here. These will be: calculateWindDirection getImage and getUserLocation.

home_controller.dart

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:getx_weather_app/services/api_services.dart'; class HomeController extends GetxController { final dateTime = DateTime.now(); RxString locationImage = ''.obs; var isDark = false.obs; dynamic currentWeather; dynamic hourlyWeatherData; var latitude = 0.0.obs; var longitude = 0.0.obs; var isloaded = false.obs; calculateWindDirection(int angle) { if (angle > 0 && angle < 22.5) { return ('N'); } if (angle > 22.5 && angle < 67.5) { return ('NE'); } if (angle > 67.5 && angle < 112.5) { return ('E'); } if (angle > 112.5 && angle < 157.5) { return ('SE'); } if (angle > 157.5 && angle < 202.5) { return ('S'); } if (angle > 202.5 && angle < 247.5) { return ('SW'); } if (angle > 247.5 && angle < 292.5) { return ('W'); } if (angle > 292.5 && angle < 337.5) { return ('NW'); } if (angle > 337.5 && angle < 360) { return ('N'); } else return ('None'); } getImage(String? name) { if (name == 'Mountain View') { locationImage.value = 'assets/images/mountainview.jpg'; return 'assets/images/mountainview.jpg'; } if (name == 'Lower Hutt City') { locationImage.value = 'assets/images/lower_hutt.jpg'; return 'assets/images/lower_hutt.jpg'; } else { locationImage.value = 'assets/images/no-image.png'; return 'assets/images/no-image.png';} } getUserLocation() async { bool isLocationEnabled; LocationPermission userPermission; isLocationEnabled = await Geolocator.isLocationServiceEnabled(); if (!isLocationEnabled) { return Future.error("Location is not enabled"); } userPermission = await Geolocator.checkPermission(); if (userPermission == LocationPermission.deniedForever) { return Future.error("Permission is denied forever"); } else if (userPermission == LocationPermission.denied) { userPermission = await Geolocator.requestPermission(); if (userPermission == LocationPermission.denied) { return Future.error("Permission is denied"); } } return await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high) .then((value) { latitude.value = value.latitude; longitude.value = value.longitude; isloaded.value = true; }); } @override void onInit() async { await getUserLocation(); currentWeather = getCurrentWeather(-41.2626, 174.9471); super.onInit(); } @override void onReady() { super.onReady(); } @override void onClose() {} }

9. Views

This will be our one and only view, here we need to inject the Home Controller into the view.

home_view.dart

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 import 'package:date_time_format/date_time_format.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:getx_weather_app/controllers/home_controller.dart'; import 'package:getx_weather_app/models/current_model.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:sizer/sizer.dart'; class HomeView extends GetView<HomeController> { HomeController homeController = Get.put(HomeController()); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.white, title: Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ Padding( padding: EdgeInsets.all(8.0), child: Text( 'Weather App', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.bold), )), Icon( MdiIcons.weatherLightning, color: Colors.blue, size: 30, ), ], ), centerTitle: true, elevation: 5, ), body: Column( children: [ SizedBox(height: 7.h), Text( homeController.dateTime.format(AmericanDateFormats.dayOfWeek), style: const TextStyle(fontSize: 16), ), const SizedBox( height: 20, ), Center( child: SizedBox( height: 70.h, width: 90.w, child: Card( elevation: 8, child: Column(children: [ Obx( () => controller.isloaded.value == true ? FutureBuilder( future: homeController.currentWeather, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { CurrentWeather data = snapshot.data; return Column( children: [ SizedBox( width: double.infinity, height: 30.h, child: Image.asset( homeController .getImage(data.name), fit: BoxFit.fill), ), SizedBox(height: 8.h), Text( "&#36;{data.name}", style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 24), ), SizedBox( height: 3.h, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( "assets/weather/&#36;{ data.weather![0].icon }.png", width: 100, height: 100, ), SizedBox( width: 10.w, ), Column( children: [ Row( children: [ const Icon( MdiIcons .sunThermometerOutline, color: Colors.grey), const SizedBox( width: 5, ), Text( "&#36;{data.main!.temp}°", style: const TextStyle( fontWeight: FontWeight .bold, fontSize: 36), ), ], ), const SizedBox(height: 5), Text( "&#36;{ data.weather![0] .description }", style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16), ), SizedBox(height: 2.h), Row( children: [ const Icon( MdiIcons .weatherWindy, color: Colors.grey), const SizedBox( width: 5, ), Text( "&#36;{ data.wind!.speed } km/h"), ], ), Row( children: [ const Icon( MdiIcons .navigationVariantOutline, color: Colors.grey, ), Text(homeController .calculateWindDirection( (data.wind!.deg! .toInt()))) ], ) ], ) ], ), ], ); } else { return const Center( child: CircularProgressIndicator(), ); } }) : const Center( child: CircularProgressIndicator(), ), ), ])))) ], )); } }

10. Lets Bring It All Together

Lastly lets modify our main file so we can see all the changes and rebuild the app with the play button.

main_model.dart

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import 'package:flutter/material.dart'; import 'package:get/get_navigation/src/root/get_material_app.dart'; import 'package:getx_weather_app/views/home_view.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:sizer/sizer.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return Sizer(builder: (context, orientation, deviceType) { return GetMaterialApp( title: 'Flutter Demo', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, textTheme: GoogleFonts.plusJakartaSansTextTheme()), home: HomeView()); }); } }

Launch and enjoy.

Please message me for any feedback, cheers.