Retrofit in Android
In the world of Android app development, making network requests to fetch data from APIs is a common task. Retrofit is a powerful and popular library that simplifies this process by providing a clean and efficient way to make API calls.
Retrofit is a type-safe HTTP client for Android and Java — developed by Square who developed Dagger, Okhttp, etc.
In this article, we’re going to explain how to use Retrofit, with a focus on its most interesting features. More notably we’ll discuss the synchronous and asynchronous API, how to use it with authentication, logging, and some good modeling practices.
Table content:
- Adding dependency
- API Modeling and annotations
- Make Retrofit Builder for Synchronous/Asynchronous API and different converters
- Observe API Data in the ViewModel
- Making a Reusable ApiInstance Class
- Authentication
- Logging
Bonus: Demo projects
Let’s start,
1. Adding a dependency
We’ll start by adding the Retrofit library:
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
Retrofit requires a minimum Java 8+ or Android API 21+.
and using Converter:
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-name:2.9.0'
/*converter-name tends to any one from listed conververts*/
using Gson Converter:
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
Check latest version here: https://github.com/square/retrofit
2. API Modeling
Retrofit models REST endpoints as Java interfaces, making them very simple to understand and consume.
We’ll model the user API from GitHub; this has a GET endpoint that returns this in JSON format:
{
login: "mojombo",
id: 1,
url: "https://api.github.com/users/mojombo",
...
}
Build model classes from response.
Retrofit works by modeling over a base URL and by making interfaces return the entities from the REST endpoint.
For simplicity purposes we’re going to take a small part of the JSON by modeling our User class that is going to take the values when we have received them:
data class User(
val login: String,
val id: Long,
val url: String
)
We can see that we’re only taking a subset of properties for this example. Retrofit won’t complain about missing properties — since it only maps what we need, it won’t even complain if we were to add properties that are not in the JSON.
Now we can move to the interface modeling, and explain some of the Retrofit annotations:
interface UserService {
@GET("/users")
suspend fun getUsers(
@Query("per_page") per_page: Int,
@Query("page") page: Int
): List<User>
@GET("/users/{username}")
suspend fun getUser(@Path("username") username: String)
}
The metadata provided with annotations is enough for the tool to generate working implementations.
The @GET annotation tells the client which HTTP method to use and on which resource, so for example, by providing a base URL of “https://api.github.com” it will send the request to “https://api.github.com/users”.
The leading “/” on our relative URL tells Retrofit that it is an absolute path on the host.
Annotations:
Retrofit provides with a list of annotations for each of the HTTP methods: @GET, @POST, @PUT, @DELETE, @PATCH or @HEAD, @Header, @Headers, @HeaderMap
Example:
@Headers({
"Content-Type: application/x-www-form-urlencoded",
"accept-encoding: gzip, deflate",
"access_token: mtlNzTVmXP4IBSba3z4XXXX"
}
)
@GET
suspend fun getUsers(): List<User>
and there are a wide variety of possible options of parameters to pass inside a method:
@Body
- Sends objects as request body.@Url
- use dynamic URLs.
@Url parameter annotation allows passing a complete URL for an endpoint.
@GET
suspend fun getUsers(@Url url: String): List<User>
@Path
specify a path parameter that will be placed instead of the markup we used in the path.
Consider this is the url:
www.app.net/api/searchtypes/862189/filters?Type=6&SearchText=School
@GET("/api/searchtypes/{Id}/filters")
suspend fun getFilterList(
@Path("Id") customerId: Long,
@Query("Type") responseType: String,
@Query("SearchText") searchText: String
): FilterResponse
So, we have:
www.app.net/api/searchtypes/{Path}/filters?Type={Query}&SearchText={Query}
Things that come after the ?
are usually queries.
@Query
- We can simply add a method parameter with @Query and a query parameter name, describing the type. To URL encode a query use the form:
@Query(value = "auth_token", encoded = true) auth_token: String
@Field
- send data as form-urlencoded. This requires a@FormUrlEncoded
annotation attached with the method. The@Field
parameter works only with a POST.
Note: @Field requires a mandatory parameter. In cases when @Field is optional, we can use @Query instead and pass a null value.
Another thing to note is that we use completely optional @Query parameters, which can be passed as null if we don’t need them, the tool will take care of ignoring these parameters if they do not have values.
Example:
interface APIInterface {
@GET("/api/unknown")
suspend fun doGetListResources():<MultipleResource>
@POST("/api/users")
suspend fun createUser(@Body user: User): User
@GET
suspend fun getUsers(@Url url: String): List<User>
@GET("/api/users?")
suspend fun doGetUserList(@Query("page") page: String): List<User>
@FormUrlEncoded
@POST("/api/users?")
suspend fun doCreateUserWithField(@Field("name") name:String, @Field("job") job: String): Response
}
3. Make Retrofit Builder for Synchronous/Asynchronous API
Retrofit
is the class through which your API interfaces are turned into callable objects. By default, Retrofit will give you sane defaults for your platform but it allows for customization.
CONVERTERS
By default, Retrofit can only deserialize HTTP bodies into OkHttp’s ResponseBody
type and it can only accept its RequestBody
type for @Body
.
Converters can be added to support other types. Six sibling modules adapt popular serialization libraries for your convenience.
- Gson:
com.squareup.retrofit2:converter-gson
- Jackson:
com.squareup.retrofit2:converter-jackson
- Moshi:
com.squareup.retrofit2:converter-moshi
- Protobuf:
com.squareup.retrofit2:converter-protobuf
- Wire:
com.squareup.retrofit2:converter-wire
- Simple XML:
com.squareup.retrofit2:converter-simplexml
- JAXB:
com.squareup.retrofit2:converter-jaxb
- Scalars (primitives, boxed, and String):
com.squareup.retrofit2:converter-scalars
Here’s an example of using the GsonConverterFactory
class to generate an implementation of the GitHubService
interface which uses Gson for its deserialization.
CUSTOM CONVERTERS
If you need to communicate with an API that uses a content-format that Retrofit does not support out of the box (e.g. YAML, txt, custom format) or you wish to use a different library to implement an existing format, you can easily create your own converter. Create a class that extends the Converter.Factory
class and pass in an instance when building your adapter.
Here’s an example of using the GsonConverterFactory
class to generate an implementation of the GitHubService
interface which uses Gson for its deserialization.
To construct an HTTP request call, we need to build our Retrofit object first:
val retrofit: Retrofit =Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(SimpleXmlConverterFactory.create())
.build()
val service:GitHubService = retrofit.create(GitHubService::class.java)
You can add client to request builder.
Retrofit.Builder()
.baseUrl(baseURL)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(SimpleXmlConverterFactory.create())
.client(OkHttpClient().newBuilder().addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
.connectTimeout(10000L, TimeUnit.MILLISECONDS)
.readTimeout(10000L, TimeUnit.MILLISECONDS)
.writeTimeout(10000L, TimeUnit.MILLISECONDS).build())
.build()
connectTimeout
Sets the default connect timeout for new connections. A value of 0 means no timeout, otherwise values must be between 1 and Integer.MAX_VALUE when converted to milliseconds.
The connect timeout is applied when connecting a TCP socket to the target host. The default value is 10 seconds.
readTimeout
Sets the default read timeout for new connections. A value of 0 means no timeout, otherwise values must be between 1 and Integer.MAX_VALUE when converted to milliseconds.
The read timeout is applied to both the TCP socket and for individual read IO operations including on Source of the Response. The default value is 10 seconds.
writeTimeout
Sets the default write timeout for new connections. A value of 0 means no timeout, otherwise values must be between 1 and Integer.MAX_VALUE when converted to milliseconds.
The write timeout is applied for individual write IO operations. The default value is 10 seconds.
4. Observe API Data in the ViewModel
We have our Retrofit object, we can construct our service call, let’s take a look at how to do this the synchronous way:
If you are using ViewModel then get Data inside view model and observe in your UI.
private val _user =
MutableStateFlow<User?>(null)
val user = _user.asStateFlow()
val service = retrofit.create(UserService::class.java)
fun getUsers() {
viewModelScope.launch {
try {
val userResponse = service.getUser("eugenp")
_user.value = userResponse
} catch (e: Exception) {
// catch exception
_user.value = null
}
}
}
5. Making a Reusable ApiInstance Class
Now that we saw how to construct our Retrofit object and how to consume an API, we can see that we don’t want to keep writing the builder over and over again.
What we want is a reusable class that allows us to create this object once and reuse it for the lifetime of our application:
class ApiInstance {
companion object {
private val retrofitBuilder: Retrofit.Builder =
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
}
private val retrofit:Retrofit = builder.build()
private val httpClient: OkHttpClient.Builder
= OkHttpClient.Builder()
fun <T> createService(serviceClass: Class<T>): T {
return retrofit.create(serviceClass)
}
}
}
All the logic of creating the Retrofit object is now moved to this ApiIntance.kt
class, this makes it a sustainable client class which stops the code from repeating.
Here’s a simple example of how to use it :
val service: UserService = ApiInstance.createService(UserService::class.java)
Now if we, for example, were to create a RepositoryService, we could reuse this class and simplify the creation.
In the next section, we’re going to extend it and add authentication capabilities.
6. Authentication
Most APIs have some authentication to secure access to it.
Taking into account our previous generator class, we’re going to add a create service method, that takes a JWT token with the Authorization header:
fun <T> createService(serviceClass: Class<T>, token: String?): T {
if (token != null) {
httpClient.interceptors().clear()
httpClient.addInterceptor( chain -> {
val original: Request = chain.request()
val request: Request = original.newBuilder()
.header("Authorization", token)
.build()
return chain.proceed(request)
})
builder.client(httpClient.build())
retrofit = builder.build()
}
return retrofit.create(serviceClass)
}
To add a header to our request, we need to use the interceptor capabilities of OkHttp; we do this by using our previously define builder and by reconstructing the Retrofit object.
Note that this a simple auth example, but with the use of interceptors we can use any authentication such as OAuth, user/password, etc.
7. Logging
In this section, we’re going to further extend our ApiInstance
for logging capabilities, which are very important for debugging purposes in every project.
We’re going to use our previous knowledge of interceptors, but we need an additional dependency, which is the HttpLoggingInterceptor from OkHttp, let us add it to our build.gradle.kts
:
implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")
implementation("com.squareup.okhttp3:okhttp:4.10.0")
Now let us extend our ApiInstance
class:
class ApiInstance{
companion object {
private val retrofitBuilder: Retrofit.Builder =
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
}
private val retrofit:Retrofit = builder.build()
private val httpClient: OkHttpClient.Builder
= OkHttpClient.Builder()
fun <T> createService(serviceClass: Class<T>) {
return retrofit.create(serviceClass)
}
}
private val logging:HttpLoggingInterceptor
= HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BASIC)
fun <T> createService(serviceClass: Class<T>): T {
if (!httpClient.interceptors().contains(logging)) {
httpClient.addInterceptor(logging)
builder.client(httpClient.build())
retrofit = builder.build()
}
return retrofit.create(serviceClass)
}
fun <T> createService(serviceClass: Class<T>, token: String?): T {
if (token != null) {
httpClient.interceptors().clear()
httpClient.addInterceptor( chain -> {
val original: Request = chain.request()
val request: Request = original.newBuilder()
.header("Authorization", token)
.build()
return chain.proceed(request)
})
builder.client(httpClient.build())
retrofit = builder.build()
}
return retrofit.create(serviceClass)
}
}
}
This is the final form of our class, we can see how we added the HttpLoggingInterceptor, and we set it for basic logging, which is going to log the time it took to make the request, the endpoint, status for every request, etc.
It’s important to take a look at how we check if the interceptor exists, so we don’t accidentally add it twice.
Demo projects:
Check api and di folders