Android API Service [Kotlin|Retrofit|Coroutines]

Nipun Ruwanpathirana
5 min readMar 31, 2020

In this short tutorial, you will learn how to write a thread safe API Service using below:

  1. Retrofit2
  2. Okhttp3
  3. Kotlin Coroutines
  4. Gson
  5. ViewModel

If you want to learn how towrite a thread safe API Service in iOS, Follow this

Add dependencies

Add below dependencies to your app level build.gradle

//Retrofit2
implementation "com.squareup.retrofit2:retrofit:<version>"
implementation "com.squareup.retrofit2:adapter-rxjava2:<version>"
implementation "com.squareup.retrofit2:converter-gson:<version>"
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:<version>"
//Okhttp3
implementation "com.squareup.okhttp3:okhttp:<version>"
implementation "com.squareup.okhttp3:logging-interceptor:<version>"
//Kotlin Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:<version>"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:<version>"
//Gson
implementation "com.google.code.gson:<version>"

SerializedStrategy for Gson (Optional)

If you need to prevent serializing or deserializing value from your request/response classes, you can follow this step.

  • Custom annotation class for SkipSerialization
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class SkipSerialization
  • SkipSerializedStrategy class that use for Gson
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes


class SkipSerializedStrategy {

companion object {
fun getStrategy(): ExclusionStrategy {
return object : ExclusionStrategy {
override fun shouldSkipClass(clazz: Class<*>?): Boolean {
return false
}

override fun shouldSkipField(field: FieldAttributes): Boolean {
return field.getAnnotation(SkipSerialization::class.java) != null
}
}
}
}
}
  • Usage
private val gson = GsonBuilder().addSerializationExclusionStrategy(SkipSerializedStrategy.getStrategy())
.create()

Internet Connectivity Checker (Optional but recommended)

If you need handle errors when no internet connectivity while trying api request, you can follow this step.

  • Write Okhttp3 Interceptor for internet connectivity.
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import java.io.IOException


class NetworkConnectionInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
if (!isConnected()) {
throw NoConnectivityException()
}
val builder: Request.Builder = chain.request().newBuilder()
return chain.proceed(builder.build())
}

fun isConnected(): Boolean {
val connectivityManager =
< you have to get applicationContext() here>.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
@Suppress("DEPRECATION")
val netInfo = connectivityManager.activeNetworkInfo
if (netInfo != null) {
@Suppress("DEPRECATION")
return (netInfo.isConnectedOrConnecting && (netInfo.type == ConnectivityManager.TYPE_WIFI || netInfo.type == ConnectivityManager.TYPE_MOBILE))
}
} else {
val network = connectivityManager.activeNetwork
if (network != null) {
val networkCapabilities = connectivityManager.getNetworkCapabilities(network)
if (networkCapabilities != null) {
return (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || networkCapabilities.hasTransport(
NetworkCapabilities.TRANSPORT_WIFI
))
}
}
}
return false
}
}
  • Custom exception for NoConnectivity
import java.io.IOException

class NoConnectivityException(message: String = "No internet connectivity!") :
IOException(message)
  • Usage
private val apiClient = OkHttpClient().newBuilder()
.addInterceptor(NetworkConnectionInterceptor())
.build()

Request and Response Classes

You can write data classes that easy to convert using gson.

  • Model Class
data class User(
@SkipSerialization
val id:Int,
var name: String?,
var email: String?
)
//Your JSON will look like
{name: "Jhon", email: "jhon@example.com"}
  • Request Class
data class UserRequest(
val deviceId: String?
val user: User
)
//Your JSON will look like
{deviceId: "XXXX" user: {name: "Jhon", email: "jhon@example.com"}}
  • Response Class
open class BaseResponse {
val message: String = ""
val success: Boolean = false
}
class UserResponse : BaseResponse() {
val user: User? = null
val token: String? = null
}
//Your JSON should look like
{token: "XXXX" user: {name: "Jhon", email: "jhon@example.com"}}

Service and Interface

  • API Service
object APIService {

private val authInterceptor = Interceptor { chain ->
val url = chain.request()
.url
.newBuilder()
.build()
val request = chain.request()
.newBuilder()
.addHeader("Authorization", AuthManager.getToken())
.url(url)
.build()
chain.proceed(request)
}
// we are creating a networking client using OkHttp and add our authInterceptor.
private val apiClient = OkHttpClient().newBuilder()
.addInterceptor(authInterceptor)
.addInterceptor(NetworkConnectionInterceptor())
.build()

private val gson =
GsonBuilder().addSerializationExclusionStrategy(SkipSerializedStrategy.getStrategy())
.create()
private val gsonConverter = GsonConverterFactory.create(gson)

private fun getRetrofit(): Retrofit {
return Retrofit.Builder().client(apiClient)
.baseUrl(BuildConfig.API_SERVER_URL)
.addConverterFactory(gsonConverter)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
}

val client: APIServiceClient = getRetrofit().create(APIServiceClient::class.java)
}
  • API Service Client
interface APIServiceClient {

@Headers("Content-Type: application/json")
@POST("v1/profile")
fun createUserAsync(@Body request: UserRequest): Deferred<Response<BaseResponse>>

@Headers("Content-Type: application/json")
@PUT("v1/profile")
fun updateUserAsync(@Body request: UserRequest): Deferred<Response<BaseResponse>>
@Headers("Content-Type: application/json")
@GET("v1/profile")
fun getUserAsync(): Deferred<Response<UserResponse>>
}

Repository Pattern

Use repository pattern to access the API Service

  • Create BaseRepository Class
open class BaseRepository {
suspend fun <T : Any> safeApiCall(call: suspend () -> Response<T>, error: String): T? {
try {
val result = apiOutput(call, error)
var output: T? = null
when (result) {
is Output.Success ->
output = result.output
is Output.Error -> Log.e("Error", "The $error and the ${result.exception}")
}
return output
} catch (noInternet: NoConnectivityException) {
val currentActivity = applicationContext().getCurrentActivity()
if (currentActivity != null) {
//No internet
}
Log.e("Error", noInternet.message)
return null
} catch (e: Exception) {
Log.e("Error", e.message)
return null
}
}

private suspend fun <T : Any> apiOutput(
call: suspend () -> Response<T>,
error: String
): Output<T> {
val response = call.invoke()
Log.i("response", response.toString())
return if (response.isSuccessful)
Output.Success(response.body()!!)
else
Output.Error(IOException("Oops .. Something went wrong due to $error"))
}
}
sealed class Output<out T : Any> {
data class Success<out T : Any>(val output: T) : Output<T>()
data class Error(val exception: Exception) : Output<Nothing>()
}
  • Create UserRepository
class UserRepository(private val apiInterface: APIServiceClient) : BaseRepository() {
//Login using safe api call
suspend fun createUser(request: UserRequest): BaseResponse? {
return safeApiCall(
//await the result of deferred type
call = { apiInterface.createUserAsync(request).await()},
error = "Error in login"
)
}
suspend fun updateUser(request: UserRequest): BaseResponse? {
return safeApiCall(
//await the result of deferred type
call = { apiInterface.updateUserAsync(request).await()},
error = "Error in login"
)
}
suspend fun getUser(): UserResponse? {
return safeApiCall(
//await the result of deferred type
call = { apiInterface.getUserAsync().await()},
error = "Error in login"
)
}
}

ViewModel and Coroutines

You can learn more about MVVM at Android Architecture Patterns Part 3: Model-View-ViewModel by Florina Muntenescu

  • In your ViewMode
class UserViewModel : ViewModel() {

//create a new Job
private val parentJob = Job()
//create a coroutine context with the job and the dispatcher
private val coroutineContext: CoroutineContext get() = parentJob + Dispatchers.Default
//create a coroutine scope with the coroutine context
private val scope = CoroutineScope(coroutineContext)

//initialize profile repo
private val userRepository: UserRepository = UserRepository(ApiService.client)

val createResponse = MutableLiveData<BaseResponse>()
val updateResponse = MutableLiveData<BaseResponse>()
val getResponse = MutableLiveData<UserResponse>()

fun create(user: User) {
///launch the coroutine scope
scope.launch {
//Login from profile repo
val request = UserRequest("XXX", user)
val result = userRepository.createUser(request)
//post the value inside live data
if ((result != null)) {
createResponse.postValue(result)
}
}
}
fun update(user: User) {
///launch the coroutine scope
scope.launch {
//Login from profile repo
val request = UserRequest("XXX", user)
val result = userRepository.updateUser(request)
//post the value inside live data
if ((result != null)) {
updateResponse.postValue(result)
}
}
}
fun create() {
///launch the coroutine scope
scope.launch {
//Login from profile repo
val result = userRepository.getUser()
//post the value inside live data
if ((result != null)) {
getResponse.postValue(result)
}
}
}
fun cancelRequests() = coroutineContext.cancel()
}

@Suppress("UNCHECKED_CAST")
class UserViewModelFactory : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return UserViewModel() as T
}
}
  • In your Activity
class UserActivity : AppCompatActivity() {
private lateinit var userViewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login_form)
val viewModelFactory = LoginViewModelFactory()
//Use view ModelFactory to initialize view model
userViewModel = ViewModelProviders.of(this@UserActivity,viewModelFactory).get(UserViewModel::class.java)
//trigger API call
userViewModel.getUser()
//Observes
userViewModel.getResponse.observe(this, Observer {
//Your Result
}
}}

From this post, we’ve learned how to write a thread safe API Service in Android.

If you want to learn how to thread safe API Service iOS, Follow this

Thank you for reading.

--

--