Modern Network calls in Android with Retrofit, Coroutines and Sealed Classes

Hyzam Ali
5 min readJan 16, 2022

--

Today we are going to take a look at how to do API requests in android the “Modern” way with some of the features that Kotlin provides alongside the Retrofit2 library.

What are we trying to create?

For this little exercise we will actually try to fetch a list of restaurants from the Yelp server for a given search term and location

Note: You will be required to create a developer account in Yelp Developers to obtain an API key that will be used for sending the request

What all dependancies do I need?

// GSON converter for serialisation
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Retrofit2 dependancy
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Coroutine dependency
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
// For launching suspended function in ViewModel scope
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"

Add the above line under your dependancies in the app/build.gradle file

Let’s get started!

Note: This blog assumes that you have knowledge of Kotlin and you will handle the View layer(Activity/fragment) yourself

  1. Setup Retrofit

Add to your src directory, the following file — services/network/RetrofitInstance.kt

const val BASE_URL = "https://api.yelp.com/v3/businesses/"
const val AUTH_HEADER = "Authorization"
const val API_KEY = "Jw0oIMgpId1" //Add Valid API Key from yelp
object RetrofitInstance {
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(
OkHttpClient()
.newBuilder()
.addInterceptor { chain ->
chain.proceed(chain
.request()
.newBuilder()
.addHeader(AUTH_HEADER, "BEARER $API_KEY")
.build()
)
}
.build()
)
.addConverterFactory(GsonConverterFactory.create())
.build()

fun getYelpServices(): YelpServices {
return retrofit.create(YelpServices::class.java)
}
}

the above code uses a Retrofit builder to return an instance of Retrofit that can be used to create API endpoints from the service interface (we will implement that later). It also adds an authorization header to all the requests sent using this retrofit instance.

2. Add a service interface file in — services/network/YelpServices.kt

interface YelpServices {
@GET("search")
suspend fun search(
@Query("term") term: String,
@Query("location") location: String = 'california',
@Query("limit") limit: Int = 50,
): SearchResponse
}

the Retrofit instance creates and endpoint implementation from this file using retrofit.create(YelpServices::class.java). The above search method send a ‘GET’ request to ‘https://api.yelp.com/v3/businesses/search’ alongside query parameters term, location and limit.

3. Setup Data Transfer Object (DTO) for our response

DTO is just a fancy word to call the Kotlin data class that models our response JSON body .Create a data class file — services/network/dto/SearchResponse.kt.

data class SearchResponse(
@SerializedName("businesses")
val businesses: List<BusinessResponse>,

@SerializedName("total")
val total: Int,
) {
data class BusinessResponse(
@SerializedName("id")
val id: String,

@SerializedName("name")
val name: String,

@SerializedName("image_url")
val imageUrl: String,

@SerializedName("is_closed")
val isClosed: Boolean,

@SerializedName("categories")
val categories: List<BusinessCategoryResponse>,

@SerializedName("price")
val price: String?,

@SerializedName("rating")
val rating: Float,

@SerializedName("phone")
val phone: String,
)

data class BusinessCategoryResponse(
@SerializedName("title")
val title: String,
)
}

@SerializedName(“key”) helps Retrofit in deciding which key in JSON response should be matched with which class property.

4. Map the DTO to an offline model to be used for business logic

It’s always a good practice to decouple the business logic from the network object that we are receiving. So let’s create a business model as well as Mappers to map our DTOs to models.

let’s start by creating a model data class in — services/offline/models/Business.kt

data class Business(
val id: String,
val name: String,
val imageUrl: String,
val isClosed: Boolean,
val categories: List<String>,
val price: String,
val rating: Float,
val phone: String,
)

Next create a Mapper interface file in — services/offline/Mapper.kt

interface Mapper {
fun toBusinessModel(
businessResponse: BusinessResponse
): Business
}

Let’s provide the implementation of this interface now in — services/offline/MapperImpl.kt

object MapperImpl : Mapper {
override fun toBusinessModel(
businessResponse: SearchResponse.BusinessResponse
): Business {
return Business(
id = businessResponse.id,
name = businessResponse.name,
imageUrl = businessResponse.imageUrl,
isClosed = businessResponse.isClosed,
categories = businessResponse.categories
.map { item -> item.title },
price = businessResponse.price ?: "$",
rating = businessResponse.rating,
phone = businessResponse.phone,
)
}
}

Now we can use the above mapper to convert network DTO response to business model in the request response.

5. Create a response wrapper for the API response

Many things could go wrong with IO operations such as not providing a valid data in the request, having backend issues or something as simple as flaky internet connections. So it’s better to have error handlers, but more often the UI don’t need to know in deep of what went wrong, it just needs to know something is wrong so it can update the screen appropriately. And that is exactly what our response wrappers will help with. It will help the View layer understand if our request was successful or a failure :(

Create a file called ApiResponse in — services/network/ApiResponse.kt

sealed class ApiResponse <T>(
data: T? = null,
exception: Exception? = null
) {
data class Success <T>(val data: T) : ApiResponse<T>(data, null)

data class Error <T>(
val exception: Exception
) : ApiResponse<T>(null, exception)

}

6. Let’s make the actual request now 🥳🥳

Create a file called MainRepository in — repositories/MainRepository.kt

object MainRepository {

suspend fun search(term: String): ApiResponse<List<Business>> {
return try {
val response = RetrofitInstance
.getYelpServices()
.search(term = term)
val businessList = response
.businesses.map {
MapperImpl.toBusinessModel(it)
}
ApiResponse.Success(data = businessList)
} catch (e: HttpException) {
//handles exception with the request
ApiResponse.Error(exception = e)
} catch (e: IOException) {
//handles no internet exception
ApiResponse.Error(exception = e)
}
}
}
  • the suspended function helps us to wait for the response to arrive and then convert the DTO to a business model to be passed to the caller.
  • the try-catch wrap helps us to catch common exceptions that might arise with the request

7. Now receive the data in the view layer with a ViewModel

class MainViewModel: ViewModel() {
private val _list = MutableLiveData<List<Business>>(emptyList())
private val list: LiveData<List<Business>> = _list

fun search(term: String) {
viewModelScope.launch(Dispatchers.IO) {
val response = MainRepository.search(term)
if (response is ApiResponse.Success) {
_list.postValue(response.data)
}
if (response is ApiResponse.Error) {
// handle error
}
}
}
}

Now you can observe the list in your views and update the data.

That’s It!

You have successfully made all the provisions required to make an API request. This approach ensures the following advantage:

  • suspended function helps to avoid callback hell and write more concise code
  • try-catch helps you to handle Api/IO exceptions gracefully
  • ApiResponse wrapper class helps you to handle success/error states in View layer
  • separate DTO ensures your business logic is not dependant on the structure of the response and any future updates on the API response structure won’t require you to refactor a major chunk of code.

Do share and leave a like if you felt this article helped you. Godspeed!

UPDATE: Have published another story that replaces Retrofit with Kotlin KTor Client as the http client for making requests. This is a more modern and platform independant approach that is powered by Kotlin. Read the full story here.

--

--