Flutter Weather App

Flutter
+

Get-X

4 Janurary 2023
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.yaml
to 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(
"${data.name}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24),
),
SizedBox(
height: 3.h,
),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Image.asset(
"assets/weather/${
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(
"${data.main!.temp}°",
style:
const TextStyle(
fontWeight:
FontWeight
.bold,
fontSize: 36),
),
],
),
const SizedBox(height: 5),
Text(
"${
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(
"${
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.