Build Your Weather Tracking Flutter App Using GetX | by Adem Gunay | Apr, 2022

Create cross-platform applications just got easier

Photo by Tudor Baciu

So you are interested in Flutter but you don’t know how or where to start to build your first app? Or, perhaps, you are interested in GetX? It’s your lucky day! You just found what you were looking for 🙂

In this article, we are going to explore how to use GetX state management to build our cross-platform Android and iOS mobile app with Flutter. Grab your coffee or any other favorite drink and let’s get started!

A Simple weather tracking app with some additional information:

What you will learn:

  1. Setup a Flutter project
  2. Make HTTP requests to retrieve data from an API
  3. Perform asynchronous tasks
  4. Define a solid and scalable base architecture
  5. Manage data in a reactive way using Get

Prerequisite

I will be using Android Studio IDE in this tutorial. If you feel more comfortable with Visual Studio Code, it works perfectly fine as well. Take your pick!

We will have to download the Flutter SDK which can be found here. You can follow the instructions on that official page, it is clear and straightforward. Once set, run the command line flutter doctor (as mentioned in the documentation) to confirm everything went well.

If you face issues while doing the setup, make sure to refer to their official documentation. You can find pretty much every solution there.

From here on, IDE-related instructions will be for Android Studio. Visual Studio Code should be similar enough.

To create a new Flutter project, click on File > New > New Flutter Project. Specify your SDK location and set your project’s location. Make sure Android and iOS are checked. You can choose the project’s name 🙂

Once the project is created, you will land on the text editor with a bunch of default code. It’s the code base of the famous click counter Flutter demo app. In order to run the demo code, you will have to create an Android or iOS emulator first. As for the iOS emulator, you will need a Mac to run it.

For creating an Android emulator, click on Tools > AVD Manager > Create Virtual Device > Pixel 4 (for example) > Choose an Android Image from Recommended > Finish

You now have a device to run the application on. Select the device on the top menu and click Run.

After compiling successfully, you should be running the Fluter demo click Counter app on your emulator and get a display similar to the image below.

Click counter app

You can take some time to explore the different repositories. At this point, you will have 3 key repositories to understand:

  • android/: this contains all the data related to native Android. If there is any Android specific setup to do, you will have to update files in this repository. IE: adding permissions in the manifest, setting versions, updating Gradle or Kotlin versions, etc.
  • ios/: this contains all the data related to native iOS code. Any iOS specific setup is done here. IE: setting up targetOS, permissions, sign in configs, etc.
  • lib/ : this is where our Flutter-related code will live. Notice the initial main.dart file which contains all the demo code.

Time to code!

Remember GetX? That’s our state management package. It basically helps us to make a reactive UI, and structure/organize our app’s code, making it easily scalable while reducing the boilerplate. To set up a package such as this one, we need to dive in pubspec.yaml file. This is where our dependencies to third party packages are set. Open the file and under dependencies: add the GetX package to the project.

dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
get: ^4.6.1

When you’re done adding this line, you will have to use the command line flutter pub get in the terminal within the project’s directory, which will download all the new dependencies code within pubspec.lock.

In Android Studio, you can see a shortcut button at the top Pub get.

Go into main.dart file and start by deleting the whole content. This file is the one we are using to run our app. The core function to start a Flutter app is the runApp() function that will create our widget tree.

By default, Flutter uses the MaterialApp root widget but the GetX package requires to change it into GetMaterialApp to take advantage of the package’s features.

At this point, your file should look like this:

Tips: if you write stless or stful and press return key, Android Studio will automatically create the boilerplate code for, respectfully, StatelessWidget and StatefulWidget for you.

What is happening in GetMaterialApp?

  • getPages: This will contain all the pages of our app. Each page widget will be wrapped within GetPage() object.
  • HomePage(): the UI for the root page (also named: ‘/’).
  • HomeBinding(): contains all the classes the Home page needs to work correctly (ie: the controller, the repositories, etc.). We will get into these later on.

First, let’s define a new HomePage widget using stless shortcut. The main widget for a new page is usually a Scaffold. We will also add some UI containing mock data to show something. We will be using Column to align ourText widgets containing some Strings vertically as well as some icons to make it prettier.

Update the HomePage widget to look like this:

And add the home_binding.dart that we did see before. This class will have to extend Bindings and we will have to overwrite the dependencies() function. In short, it is how we inject dependencies in get. Create the class with the following code:

import 'package:get/get.dart';class HomeBinding extends Bindings {
@override
void dependencies() {
// TODO: We will add the dependencies later.
}
}

⚠️ Make sure to import these 2 new classes into the main.dart and then run the app and you should have something like this in your emulator:

If you look carefully, you can see that our Row‘s items are repeated multiple times. We will extract the code into a separate widget, later on, to make it reusable and avoid repetition.

You might also have noticed the SafeArea widget Its purpose is to define the UI’s boundaries. Usually, devices require spacing for the home button at the bottom as well as spacing at the top for status bars.

The SafeArea widget

We have our UI but we want to fetch some data to make it dynamic. Let’s create a new Controller class called home_controller.dart inside the lib/ folder.
The controller has to extend GetxController to take advantage of the Get library.

Your controller will look like this:

import 'package:get/get.dart';

class HomeController extends GetxController {

getCurrentLocation() {
// TODO add the logic to fetch current city
}

getTemperatureForCurrentLocation() {
// TODO add the logic to fetch temperature for current city
}
}

We will need 2 data models at this point. The data model will keep the city information and another data model that will keep the weather information.

First, let’s understand how to get the current city information. We are going to use the endpoint http://ip-api.com/json/. This request is giving us data based on the user’s IP address. This is not very accurate as it depends on the internet provider’s location but it allows us to ignore location permission requests. An example of using Canadian IP address would be:

{
"status": "success",
"country": "Canada", // We need this
"countryCode": "CA",
"region": "QC",
"regionName": "Quebec", // We need this
"city": "Montreal",
"zip": "H3G",
"lat": 45.4995, // We need this
"lon": -73.5848, // We need this
"timezone": "America/Toronto",
"isp": "Le Groupe Videotron Ltee",
"org": "Videotron Ltee",
"as": "AS5769 Videotron Telecom Ltee",
"query": "24.48.0.1"
}

Let’s create the model called location_data.dart that will keep the location information.

NOTE: The .fromJson function’s purpose is to convert the JSON data we get from the HTTP request into a dart object that we can use within our app.

Our next task is to make the http request within our app. In order to do that, we need the help of another package called http package, the official one from the Flutter team. ⚠️ Don’t forget that every time we need a third-party library, we need to add it into our pubspec.yaml file followed by a flutter pub get command to retrieve the package’s code.

dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
get: ^4.6.1
http: ^0.13.4

Once we did add this package, we can use its code to make our HTTP requests to an endpoint and retrieve the JSON object.

Let’s add a repository that will do the call and prepare the data for our controller. Create a repository called weather_repository.dart as below:

Note: This function’s body is wrapped within async and returns a Future<LocationData?> type. It means this operation will run asynchronously and will take some time to complete. It will return a nullable LocationData object in the future. The await Tells the function to stop and wait to get the endpoint’s data before continuing (the time consuming operation is that specific line).

Now, remember the Binding that is supposed to hold all the requirements for the UI to work correctly? We HomePage is going to need WeatherRepository and the HomeController. We have to instantiate these 2 objects inside the dependencies function. Let’s update the Binding as follow:

import 'package:get/get.dart';
import 'package:my_weather_tracker/module/home_controller.dart';
import 'package:my_weather_tracker/repository/weather_repository.dart';

class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => WeatherRepository());
Get.lazyPut(() => HomeController());
}
}

Get.lazyPut(() => abc()) is a function that will create an instance of the object whenever it is required. For example, when we make a call upon their methods, if the object doesn’t exist yet, we will create one. If it was already created, we will use the existing one.

Now we can import this repository into our controller and use getCurrentLocation function within it. Update your controller like this:

NOTE: The Rxn object is an observable but with nullable type. In this case, since we will not know the location until we make the request, it’s initially set to null. More Rx types here.

Now, all we have to do is to update our UI with the retrieved value. We have to get an instance of this controller within the home_page.dart file and observe the changes on locationData to display the user’s current city.
Add this line under the HomePage() constructor:

final HomeController _controller = Get.find();

Note: the prefix underscore on the variable’s name makes it private.

To make our UI reactive to our controller’s changes, we need to wrap the SafeArea widget with an Obx widget This will notify and update SafeArea‘s content whenever an observable’s value changes.

///...
child: Obx(
() => SafeArea(
child: Column(
///...

Now we can add the controller’s address variable in the Text. Since address is referencing an Rx observable, the Obx wrapper higher in the widget tree will get triggered on its value change and it will notify the UI to recreate the widget with the new value.

//...
Expanded(
// We will fetch the user's current city
child: Text(
"Your location is ${_controller.address}",
style: const TextStyle(fontSize: 18),
),
),
//...

NOTE: if you get any error, try removing some const keyword as they cannot be constants anymore.

Try running your app and you should see the city now!

We need to know the temperature at this point and to get that data, we are going to use Open Weather Map API. Make sure to create an account to get a free API key. You will need it to make HTTP requests.

Here is a snippet of the data response and the properties we will need from that API call:

{
//...
"main": {
"temp": 280.58, // What we need, default unit is Kelvin.
"feels_like": 280.03,
"temp_min": 278.5,
"temp_max": 281.33,
"pressure": 1011,
"humidity": 84
},
//...
}

Let’s create a WeatherData class to contain weather-related information:

And now, to retrieve this data, we will add this new getWeatherForLocation function into our WeatherRepository. The API will request some additional data such as, latitude, longitude, etc. that we are going to pass inside params. We will parse the JSON response into a WeatherData object.

Now we need to tap into it to retrieve the temperature in our controller.

Finally, we have to use this new temperature variable within the UI Widget HomePage:

///...
Expanded(
// We will use the city data to fetch the weather
child: Text(
"The temperature is ${_controller.temperature}°C",
style: const TextStyle(fontSize: 18),
),
),
///...

Run the app and tada 🎉

We will complete the last row item, which is an informative text based on the weather. Of course, that should depend on multiple factors (humidity, pressure, max and min temperatures, etc.) but for simplicity’s sake, let’s use only temperature. We will have the following 4 scenarios:

  1. temperature ≤ 0 → “make sure to dress thick cloths! It’s freezing out there!”
  2. temperature ≤ 15 → “wear a jacket, don’t catch a cold!”
  3. else → “enjoy the weather, it’s nice!”
  4. temperature is null → “unknown”
/// inside HomeController
_getInfoText(int? temperature) {
if (temperature == null) {
infoText.value = "unknown";
} else if (temperature <= 0) {
infoText.value =
"make sure to dress thick cloths! It's freezing out there!";
} else if (temperature <= 15) {
infoText.value = "wear a jacket, don't catch a cold!";
} else {
infoText.value = "enjoy the weather, it's nice!";
}
}

Update the HomeController with a new variable as follow and add it to the widget.

///...// A reactive String to display informative text. default is '...'.
RxString infoText = '...'.obs;
///...getTemperatureForCurrentLocation() async {
// Verify if location is not null first
if (locationData.value != null) {
// We assign the response from our API call to our Rx object.
weatherData.value =
await _repository.getWeatherForLocation(locationData.value!);
_getInfoText(weatherData.value?.temp); // make the call here
}
}

The last task is to update our UI with a new observable and that’s it!

///...
Expanded(
// We will give advice on what to do
child: Text(
"You should ${_controller.infoText.value}",
style: TextStyle(fontSize: 18),
),
),
///...

Let’s see the result!

The last thing here is to refactor our row widgets. We can clearly notice a pattern in each of them. A leading Icon and a trailing Text within a Container. We will create our own RowItem widget which will be an assembling of the previously listed widgets.

Much better and more readable right? Always remember to try to extract the same patterns into a different widget. You can even make these widgets global by creating a new file containing a StatelessWidget called RowItem. This will allow you to reuse that row custom widget anywhere within the app.

That StatelessWidget RowItem would look like this:

Try replacing _rowItem with this new RowItem and you will see that it works exactly the same 😉

///...
RowItem(
icon: Icons.thermostat_outlined,
text: "The temperature is ${_controller.temperature}°C",
),
///...

Feel free to check out the whole project which is available on my GitHub 🚀

Leave a Comment