APIs, Flutter and GetX
Learning Objectives
- You know how to use an API in a Flutter application.
In the previous chapter, we created a service for fetching jokes from an API. The service was implemented as follows.
class JokeService {
Future<Joke> getRandomJoke() async {
final response = await http.get(
Uri.parse('https://simple-joke-api.deno.dev/random'),
);
final data = jsonDecode(response.body);
return Joke.fromJson(data);
}
}
class Joke {
String setup;
String punchline;
Joke()
: setup = '',
punchline = '';
Joke.fromJson(Map<String, dynamic> map)
: setup = map['setup'],
punchline = map['punchline'];
}
Let’s next look into how to use the service in a Flutter application. As a part of the process, we’ll again also rehearse using features from GetX.
Creating a controller
Let’s first create a controller that is responsible for fetching jokes from the API. The controller will hold the joke that has been fetched and a boolean value that indicates whether a joke has been fetched or not.
class JokeController {
Joke? joke;
var hasJoke = false.obs;
void fetchJoke() async {
hasJoke.value = false;
joke = await JokeService().getRandomJoke();
hasJoke.value = true;
}
}
In the above, the Joke? joke
creates a nullable object that can hold a joke or be null
. This is a part of Dart’s null safety feature, which allows explicitly creating objects that may be null. To use properties of an object that has been set as nullable, we have to explicitly state that we know that the variable is not null, which is done with an exclamation mark, e.g. joke!.setup
.
Showing a joke
With the above controller, we can create a widget for showing a joke. The widget will have a button that, when clicked, fetches a joke from the API and shows it to the user.
class JokeScreen extends StatelessWidget {
final controller = Get.find<JokeController>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
ElevatedButton(
onPressed: controller.fetchJoke,
child: const Text('Fetch joke!'),
),
Obx(
() => controller.hasJoke.value
? Text(
"${controller.joke!.setup} ${controller.joke!.punchline}")
: Text(""),
)
],
),
),
);
}
}
Note that the above widget relies on observing changes in the hasJoke
property of the controller. When the value of hasJoke
changes, the widget is re-rendered, and the joke is shown to the user.
With the above in place, we can create an application that fetches jokes from an API and shows them to the user. The application is started by adding the JokeController
to the GetX dependency injection container and then showing the JokeScreen
. The application as a whole would look as follows.
When running Flutter applications in a browser, the APIs that can be accessed are restricted by the Cross-Origin Resource Sharing policies. APIs that do not allow Cross-Origin Resouce Sharing do not work through a browser — for mobile applications, desktop applications, etc that do not run within a browser, this restriction is lifted.
This is discussed in more detail in the Web Software Development course.
Load on application initialization
To load a joke when the application is initialized, we can add functionality to the JokeController
class. Here, the class GetxController from GetX becomes useful — the GetxController has a method onInit
that is called by GetX when a controller inheriting the class is initialized.
In our case, we can adjust the JokeController
to extend the GetxController
class, and override the onInit
method to fetch a joke when the controller is initialized. This would be done as follows.
Note that we need to also call the super.onInit()
method to ensure that the controller is properly initialized.
class JokeController extends GetxController {
Joke? joke;
var hasJoke = false.obs;
@override
void onInit() {
super.onInit();
fetchJoke();
}
void fetchJoke() async {
hasJoke.value = false;
joke = await JokeService().getRandomJoke();
hasJoke.value = true;
}
}
With the above in place, the application will fetch a joke from the API when the controller is initialized, and show the joke to the user. Now, the application as a whole would look as follows.
Lengthy wait times
If an API takes a long time to respond, it is meaningful to add functionality to inform the user about the situation. To illustrate this, let’s adjust the JokeService
to have a 5 second delay before returning a joke. This can be done using Future.delayed constructor jointly with an instance of Duration as follows.
class JokeService {
Future<Joke> getRandomJoke() async {
await Future.delayed(Duration(seconds: 5));
final response = await http.get(
Uri.parse('https://simple-joke-api.deno.dev/random'),
);
final data = jsonDecode(response.body);
return Joke.fromJson(data);
}
}
Now, fetching a joke will take 5 seconds, and the user should be informed about the situation. This can be done by adding a loading indicator such as CircularProgressIndicator to the JokeScreen
widget.
With a loading indicator and a 5 second delay, the application would look as follows.
Note that the automated tests in exercises wait for changes in the user interface and then check for outcomes. Progress indicators provide constant changes in the user interface and can cause problems to the tests. When returning applications for assessment, do not use progress indicators or other animations that continuously update what is shown — instead, e.g. use a text “Loading” or similar.
Errors and error handling
When working with APIs, it is important to consider error handling. In the following example, we adjust the JokeService
to throw an error with a 50% probability. This is done by using the Random class from the dart:math
package.
class JokeService {
Future<Joke> getRandomJoke() async {
await Future.delayed(Duration(seconds: 5));
if (Random().nextDouble() < 0.5) {
throw Exception("Random error!");
}
final response = await http.get(
Uri.parse('https://simple-joke-api.deno.dev/random'),
);
final data = jsonDecode(response.body);
return Joke.fromJson(data);
}
}
Now, fetching a joke has a 50% chance of throwing an error. To handle the error, we can adjust the JokeController
to catch the error using a try-catch block, as shown below.
// ...
void fetchJoke() async {
hasJoke.value = false;
try {
joke = await JokeService().getRandomJoke();
} catch (e) {
// Inform the user about the error
joke = Joke();
}
hasJoke.value = true;
}
// ...
Next, we need to inform the user about the error. An increasingly common way is to show an error using a snackbar, which is a temporary message that is shown to the user. With GetX, a snackbar can be created and shown using the Get.snackbar method, which both creates a snackbar and shows it to the user.
The method takes a title, a message, and optional extra information such as a widget and background color. In the following, we adjust the JokeController
to catch the error and show the user a snackbar with the error message.
// ...
void fetchJoke() async {
hasJoke.value = false;
try {
joke = await JokeService().getRandomJoke();
} catch (e) {
Get.snackbar("Error", e.toString());
joke = Joke();
}
hasJoke.value = true;
}
// ...
Now, the application will catch errors that occur when fetching a joke and inform the user about the situation. The application as a whole would look as follows.