Making Flutter and REST API Work Together — Preventing Application Freezing (Part 3)
One of the most critical mistakes any application or graphical interface can make is freezing. We’re not referring to the interface freezing during data loading from the internet or screen rendering, as this is usually resolved within microseconds, and the user may not even notice the loading time. However, there are many factors beyond the developer’s control that can increase loading times to seconds or even tens of seconds. As a result, users may become bored and assume that the application is not working, prompting them to restart, reinstall, or worse, delete the application. What can be done to prevent this issue?
The following posts are suggested reading for this post to get you started:
- 👉 Part 1: Creating API Provider
- 👉 Part 2: Debugging with Unit Tests
Creating First Screen
First, let’s create the weather_page.dart
screen — a class inherited from StatefulWidget
— a class with its own state…blah-blah-blah. If you would like me to write an article about the basic Flutter classes, please let me know in the comments.
It looks like this:
import 'package:flutter_api/api_data_provider.dart';
import 'package:flutter/material.dart';
class WeatherPage extends StatefulWidget {
@override
WeatherPageState createState() => WeatherPageState();
}
class WeatherPageState extends State<WeatherPage> {
final ApiDataProvider apiDataProvider = ApiDataProvider();
late Future<ForecastResponse> response;
@override
void initState() {
super.initState();
response = apiDataProvider.getWeather();
}
Important: It is worth noting that the return type of the
getWeather()
method is aFuture
. AFuture
is not an object, but rather a state of an asynchronous operation.
In the main.dart
file, we make our screen the home screen:
home: WeatherPage()
We create an instance of a class to work with the API, as well as an instance of an object that will store the result of the API method call.
TL;DR
Note: In Flutter, there is no such thing as multithreading. Everything works on the basis of interruption. This is how the processor works. By performing several operations in parallel, the processor, for example, when data is being written to memory and its participation is not needed, switches to the second operation, and when it is not needed in the second operation, it switches back to the first. Multithreading in modern computers is implemented through multi-core processors, where each core is a separate processor. In Flutter, it is not possible to draw the interface and send a request at the same time, which is why the concept of asynchrony and two magic words, “async” and “await,” emerged. With “async,” we tell the compiler that there will be a breakpoint in this method, and with “await,” we specify where exactly. The general idea is that the invoked method is executed, and as soon as we reach “await,” the method is paused, and we move on to another task, such as drawing the interface. Once the interface is complete, we return to “await,” and if the method near it has executed, we continue executing the originally invoked method.
Therefore, it is necessary to simply handle these states in order to provide the user with the appropriate content at each stage. While the operation is being performed, we display a loading animation, if there is an error, we display an error message, and if the operation is successfully completed, we display the result.
Note: In the article, I will be using the standard loading animation widget. However, nowadays, a more popular approach is to use a skeleton or, more accurately, a shimmer loading animation. This is when the outlines of the final screen are filled with a color that shimmers. We won’t be covering this in detail, but if you’re interested, there’s an official tutorial on how to implement it.
The method call looks like this. Let’s go through it line by line:
- Line 10 creates an instance of a class to work with the API.
- Line 11 creates an object that will store the state of an asynchronous API call.
Important: The
response
object is marked aslate
. This is a feature of Dart’s null safety implementation. At any given time, the object must have a value, or it must be marked as nullable. The keywordlate
tells the compiler that we guarantee that the object will have a value by the time thebuild()
method is called. So, during the initialization of our screen’s state ininitState()
(which is called first in the widget lifecycle), we set a value for the response object.
- On line 14, we override the
initState()
method, or more precisely, supplement it with our own logic, by calling the original method beforehand. - On line 17, we call the
getWeather()
method and store its state in theresponse
variable.
Alright
We’ve created the screen and called the method. What’s next? How do we handle the states of the asynchronous operation?
To do this, Flutter provides a special widget called FutureBuilder
. It would look something like this:
- Line 25 adds the
FutureBuilder
to the widget tree. - Line 26 indicates where the state that needs to be monitored is located.
- On line 28, we create a variable to store whether the data has been loaded or not.
Note: For our current task, it is not necessary, and we could just check the
snapshot.hasData
flag instead ofisReady
. However, in the future, if we need to add the ability to request data again, our object will continue to hold the data, and when the asynchronous method completes, it will be updated. But without a trigger to start the loading animation, we will end up with a frozen interface. TheconnectionState
property comes to the rescue, which will be either waiting or active at the moment when theFuture
is restarted.
- On line 30, our data has loaded successfully, so we display it on the screen.
- On line 35, an error occurred, so we display the error message.
- Line 39 — None of the above conditions were met, so we show the loading indicator.
Here’s what it would look like:
Summary
We can now connect to an API in our application and process simple GET requests without causing the application to freeze. In the next articles, we will explore how to send POST requests, create classes for deserialization based on server swagger documentation, and discuss code reviews.
Subscribe and don’t miss new materials!
- 👉 Read Part 4: Sending POST Request
- 👉 Read Part 5: Code Review
- 👉 Read Part 6: Building Dart Types from Swagger/OpenAPI Schemas
- 👉 Read Part 7: Efficient Pagination
- 👉 Read Part 8: Efficient Data Merging
- 👉 Read Part 9: Optimizing Search
- 👉 Read Part 10: Implementing Filtering
- 👉 Read Part 11: Seamless Data Caching Integration