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.
Jokes and Punchlines
0 / 30 points
Modifying the Joke application you’ve seen above, create a version where the user is not shown the punchline immediately. Upon start, the application should show an ElevatedButton with the text “Fetch joke!”. At this point, no requests to the Joke API should have been made.
When the user presses the button, the application should retrieve a joke from the Joke API at https://simple-joke-api.deno.dev/random. When a joke has been retrieved, the application should show the setup of the joke to the user, and an additional ElevatedButton with the text “Show punchline”. When the user presses the button, the application shows also the punchline of the joke. The button with the text “Fetch joke!” remains visible and can be used at any point.
Pressing the button “Fetch joke!” again fetches another joke from the API.
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.
Currency conversions
0 / 80 points
Write a currency conversions application that uses the currency API at https://api.frankfurter.app/latest. Upon start, the application should show two ElevatedButton widgets, one with text “100 EUR in USD” and one with text “100 EUR in SEK”. In addition, the application should have a Text widget with the text “Press to retrieve”.
When the button with the text “100 EUR in USD” is pressed, the application should make a request to the address https://api.frankfurter.app/latest?from=EUR&to=USD and retrieve the rate from the response. After this, the rate, multiplied by 100, should be shown in the application, replacing the text “Press to retrieve”.
Similarly, when the button with the text “100 EUR in SEK” is pressed, the application should make a request to the address https://api.frankfurter.app/latest?from=EUR&to=SEK and retrieve the rate from the response. After this, the rate, multiplied by 100, should be shown in the application, replacing the text “Press to retrieve” (or an existing rate).
Finally, if there’s an error when retrieving the rate, the application should show the error message “Failed to retrieve the rate, try again later.” instead of the rate (in a Text widget). The error message should be shown for both conversions.
Note! Do not use a progress indicator (or another animation) for indicating that the application is waiting for a response from the API.