When working on an project, you might find it interesting to use dynamic data from the web. For example, you might want to display the weather forecast, the latest news, or a stream of bits for an image. This is where web APIs come in.
Let's do a rundown of the most common things to keep in mind when working with web APIs!
APIs and external data are not under your control. They take time to load, they might return unexpected results, or they might simply be down. It is important to keep this in mind when using them.
You should read the documentation of the API you are using, and make sure you understand how it works. Both the request format and the response format are important.
Note
JSON is a common format for APIs. It is a way to represent data in a human-readable format. It is similar to a dictionary in Python, or a map in Java. For example, here is how a server could respond to the following JSON-encoded request:
// Client request
{
"id": 3110,
}
// Server response
{
"id": 3110,
"username": "Truddy",
"last_login": "1677016003",
"is_admin": false,
"avatar": {
"url": "https://example.com/avatar.png",
"width": 128,
"height": 128
}
}
Sometimes, an API will require you to register an account, and provide an API key. This is usually done to prevent abuse of the API. You might need to specify this key in the request headers if you build requests manually (for example, requests to the OpenWeatherMap API require an API key).
When you make a request to an API, you will get a response code. This indicates basic information about the status of the response from the server. For instance, recieving a 200 OK
response code means that the request was successful. Any other response code may indicate an error.
It is a good idea to check this response code, and handle it appropriately. For example, if you get a 404 Not Found
response code, you might want to perform a specific action.
Worse, if the API is down, or if the user is offline, you should handle this gracefully. For example, you could display a message to the user, or use a cached version of the data.
Whether you repeatedly ask for large amounts of data, or just accounting for network issues, it is a good practice to think in advance and to some cache data locally.
A simple way to cache basic data is to use SharedPreferences. Shared Preferences is a commonly used mechanism for storing small amounts of data in Android. It's a key-value store that allows you to save primitive data types (such as strings, booleans, and integers) and retrieve them later.
For storing data in a database like structure, you can use Room, or refer to this exercise from the Software Engineering course.
Finally, for more types of data, you should check out the Android data storage guide.
You shouldn't be running I/O tasks on the main thread, as that thread is also responsible for UI updates. If the main thread is blocked, the user will see a frozen app, and might get impatient and close it.
Take a look at Kotlin coroutines (or equivalent asynchronous Java functions, such as the fancy Futures as seen in the Asynchrony lecture).
There are many libraries that can help you with API requests. For example, Retrofit is a popular library for making HTTP requests. It is built on top of OkHttp, which is a popular HTTP client. They allow you to make requests in a simple and concise way.
Let's try to make a simple activity that fetches a random challenge from the BoredAPI: when heading over to https://www.boredapi.com/api/activity, this API returns a random activity that you can do when you're bored. We'll use Retrofit to make the request, in Kotlin.
Create a new project in Android Studio. You can use the Empty Activity template.
Before we code, don't forget to add the following dependencies to your build.gradle
file (in the app
module):
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'
implementation 'com.google.code.gson:gson:2.8.7'
Create a new Kotlin class called BoredActivity
. This class will represent the data we get from the API.
data class BoredActivity(
// Check the response format of the response! See the documentation
val activity: String,
// TODO: ...
)
In Retrofit, you define an interface that describes the API you want to call. To call the BoredAPI API, you can define an interface like this:
interface BoredApi {
@GET("activity")
fun getActivity(): Call<BoredActivity>
}
By using the @GET
we specify the type of request. The activity
parameter is the endpoint of the API. The getActivity()
method returns a Call
object, which is a Retrofit class that represents an HTTP request. This Call
object will be used to make the request.
Now, we need to create an instance of the BoredApi
interface. We can do this by using the Retrofit.Builder
class.
val retrofit = Retrofit.Builder()
.baseUrl("https://www.boredapi.com/api/")
.addConverterFactory(GsonConverterFactory.create())
.build()
The baseUrl
parameter is the base URL of the API. The addConverterFactory
method specifies how the response will be converted to an object. In this case, we use GsonConverterFactory
(the G instead of J is intentional, it is named after Gson, a library that handles JSON). This will convert the response to a BoredActivity
object.
Tip
You don't need to store all value fields in the response from the server into the data class BoredActivity
, you can keep a subset of those that you want. Retrofit won't complain about missing properties since it only maps what we need, and it won't even complain if we were to add properties that are not in the JSON response.
Now, we can create an instance of the BoredApi
interface:
val boredApi = retrofit.create(BoredApi::class.java)
Let's make the request. We can do this by calling the getActivity()
method on the boredApi
object. This method returns a Call
object, which we can use to make the request.
boredApi.getActivity().enqueue(object : Callback<BoredActivity> {
override fun onResponse(call: Call<BoredActivity>, response: Response<BoredActivity>) {
// TODO: Handle the response
}
override fun onFailure(call: Call<BoredActivity>, t: Throwable) {
// TODO: Handle the error
}
})
With the stubs we provided, you are now be able to make a simple app that fetches a random activity from the BoredAPI.
- Add a button that will make the request to the API when clicked and fetch the data.
- Add a text view that will display this activity. Simpy use the
activity
field of theBoredActivity
object, or you can get creative and display the other fields as well, add new textviews for other information, etc.
Is this code blocking the UI thread? (think about it, then click to see an answer)
No, because we are using the `enqueue` method, which will make the request in the background. If we were using the `execute` method, the request would be made on the main thread, and the UI would freeze.Now imagine you lose connection to the Internet (you can simulate this by turning off your Wi-Fi and data connections). What happens?
Let's expand our app to handle this case. We could do this by using Room, or with the quick and dirty SharedPreferences, but it is up to you.
For this part:
- For each time you make a request to the API, you should store the response in the local database.
- When you lose connection to the internet (failing request), load the data from the local database, select a random activity, and display it to the user. Don't forget to tell the user that you're using cached data!
Test your app by clicking the button a couple of times, then disabling your Internet connection, and keep clicking the button.
When testing, you will connect to a fake server, and you will test that your app behaves correctly when the server returns a response, and when it returns an error.
A mock web server imitates a real server without making internet requests. It allows testing of APIs without hitting rate limits or worrying about too much delay, API unavailablity or network issues. It intercepts HTTP requests, processes them offline, and returns specified data.
Now, try using MockWebServer to test your app! It was developed by the same people who made Retrofit.
In your viewmodel, it is a good workaround to inject the BoredApi
object, so that you can easily replace it with a mock object when testing. Take a look at Hilt to see how to automatically inject the right object (testing vs production) in your viewmodel.