Using Retrofit with Kotlin Coroutine in Android

Photo by Uliana Kopanytsia on Unsplash
Photo by Uliana Kopanytsia on Unsplash
We often use Retrofit as an HTTP client to communicate with RESTful APIs in Android. Kotlin coroutine can make Retrofit easier to use. This chapter will explain how to use Retrofit with coroutine.

We often use Retrofit as an HTTP client to communicate with RESTful APIs in Android. Kotlin coroutine can make Retrofit easier to use. This chapter will explain how to use Retrofit with coroutine.

The complete code can be found in .

Dependencies

In order to use Retrofit and coroutine, we need to add the following dependencies to the project’s build.gradle. The first is to import coroutine, and the other two are to import Retrofit and its Gson converter.

dependencies {
    ...

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

    ...
}

Postman Echo API

We will use Postman’s Echo API as our backend. The POST request URL of the Echo API is https://postman-echo.com/post. We will send the following JOSN data.

{
    "name": "wayne",
    "sex": 1
}

The Echo API will response the following JSON data.

{
    "args": {},
    "data": {
        "name": "wayne",
        "sex": 1
    },
    "files": {},
    "form": {},
    "headers": {
        "x-forwarded-proto": "https",
        "x-forwarded-port": "443",
        "host": "postman-echo.com",
        "x-amzn-trace-id": "Root=1-60af6a1c-4b2e2a28669e915137e9fed3",
        "content-length": "37",
        "content-type": "application/json",
        "user-agent": "PostmanRuntime/7.28.0",
        "accept": "*/*",
        "cache-control": "no-cache",
        "postman-token": "c59277d0-4d31-4b48-ab3c-8b0ab1eee9a6",
        "accept-encoding": "gzip, deflate, br"
    },
    "json": {
        "name": "wayne",
        "sex": 1
    },
    "url": "https://postman-echo.com/post"
}

Kotlin Coroutine

We will use Kotlin coroutine in this article. If you are not familiar with coroutine, you can refer to the following article first.

Creating Retrofit Service

Add Service.kt and declare interface Service in it. Then, declare post() method to correspond to the /post API. Note that the post() method must be declared as suspend.

package com.waynestalk.example

import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST

interface Service {
    data class PostRequest(
        val name: String,
        val sex: Int,
    )

    data class PostResponse(
        val data: PostRequest,
        val json: PostRequest,
        val headers: Map<String, String>,
        val url: String,
    )

    @POST("/post")
    suspend fun post(@Body request: PostRequest): Response<PostResponse>
}

Next, add Server.kt and declare object Server in it. In Server, we create a Retrofit instance, and set the backend URL and Gson converter that converts JSON strings into objects. In this way, the Retrofit part is roughly completed.

package com.waynestalk.example

import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object Server {
    private const val URL = "https://postman-echo.com"

    private val service: Service

    init {
        val client = OkHttpClient.Builder().build()
        val retrofit = Retrofit.Builder()
            .baseUrl(URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build()
        service = retrofit.create(Service::class.java)
    }
}

Using Retrofit with Coroutine

After Retrofit is setup, the next step is to call Retrofit to create a request and obtain data. Declare post() method in Server to call Service.post(). Because Server.post() mainly calls Service.post() and obtains data, we hope it will never be executed in the main thread. Therefore, we wrap the entire method in withContext(Dispatchers.IO), so that no matter which coroutine context the caller is in, Server.post() will be executed under Dispatchers.IO. Note that Server.post() must also be declared as a suspend method.

package com.waynestalk.example

import android.util.Log
import kotlinx.coroutines.Dispatchers
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import kotlinx.coroutines.withContext

object Server {
    ...

    private val tag = Server::class.java.name

    suspend fun post(name: String, sex: Int): Pair<String, Int> = withContext(Dispatchers.IO) {
        Log.d(tag, "Thread is ${Thread.currentThread().name}")

        val request = Service.PostRequest(name, sex)
        val response = service.post(request)
        if (response.isSuccessful) {
            val body = response.body()!!
            return@withContext Pair(body.json.name, body.json.sex)
        } else {
            throw Exception(response.errorBody()?.charStream()?.readText())
        }
    }
}

Calling Server.post()

Finally, let’s take a look at how to call Server.post() in an Activity. In the following code, we can see that the code is quite concise. We don’t need to switch to Dispatchers.IO to call Server.post(), and then switch back to Dispatchers.Main to refresh UI.

In addition, note that the code is presented in a synchronous manner, but it is executed asynchronously. Coroutine makes the code more streamlined and simpler.

class MainActivity : AppCompatActivity() {
    private val tag = MainActivity::class.java.name

    private lateinit var postButton: Button
    private lateinit var nameTextView: TextView
    private lateinit var ageTextView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        postButton = findViewById(R.id.postButton)
        nameTextView = findViewById(R.id.nameTextView)
        ageTextView = findViewById(R.id.ageTextView)

        postButton.setOnClickListener {
            CoroutineScope(Dispatchers.Main).launch {
                val (name, sex) = Server.post("Wayne", 1)
                Log.d(tag, "Thread is ${Thread.currentThread().name}")
                nameTextView.text = name
                ageTextView.text = if (sex == 1) "male" else "female"
            }
        }
    }
}

Conclusion

Using Retrofit with Kotlin coroutine makes the code much simpler. Coroutine’s withContext() also allows a method to be executed in a specified context. This way, you don’t have to worry about developers accidentally executing IO in the main thread.

9 comments
  1. It is part of API:

    app.post(‘/addfrutos’, (req, res) =>{
    const sql = ‘insert into fruto set ?’;
    const customerObj = {
    cod_fruto: req.body.cod_fruto,
    cod_planta: req.body.cod_planta,
    descripcion: req.body.descripcion,
    fecha_recoleccion: req.body.fecha_recoleccion,
    peso: req.body.peso
    }

    connection.query(sql, customerObj, error =>{
    if (error) throw error;
    res.send(‘Fruit created!’);

    });

    });

      1. oh! In the response of you example you received:
        nameTextView.text = name
        ageTextView.text = if (sex == 1) “male” else “female”

        two string declared in: (( val headers: Map,))?? (in the interface)

        and i only one?
        i go try it

  2. Hi, a dude… In object Server … i have:

    val request = Service.PostRequest(cod_planta, descripcion, fecha_recoleccion, peso)
    println( request); ==> PostRequest(cod_planta=1, descripcion=La ostia, fecha_recoleccion=2012-02-16, peso=5.0)

    //I have a error in the next instruction:
    val response = service.post(request)

    error:
    E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
    Process: com.proyectoFM.helloqr, PID: 9256
    com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $

    My interface is:
    package com.proyectoFM.helloqr

    import retrofit2.Response
    import retrofit2.http.Body
    import retrofit2.http.POST
    interface Service {
    data class PostRequest(
    val cod_planta: Int,
    val descripcion: String,
    val fecha_recoleccion: String,
    val peso: Double
    )
    data class PostResponse(
    val data: PostRequest,
    val json: PostRequest,
    val headers: Map,
    val url: String,
    )
    @POST(“/addfrutos”)
    suspend fun post(@Body request: PostRequest): Response
    }

    ¿Any idea of error? Thanks.

    1. Hi, I think it should be incorrect JOSN format of the response. You can send a request with Postman and see the response body before you parse it to JSON.

      1. Hi Thank you very much for answering me, in postman all is ok:
        POST: http://XX.XXX.XXX.XXX:XXX/addfrutos

        {
        “cod_planta”: “1”,
        “descripcion”: “rica planta”,
        “fecha_recoleccion”: “1981-02-26”,
        “peso”: “15.5”
        }
        RETURN: Fruit created!

        AND FORMAT OF MARIADB TABLE IS:
        cod_fruto: int not null Auto_increment unsigned
        cod_planta: int
        descripcion: mediumtext
        fecha_recoleccion: date default curdate()
        peso: decimal(6,2)

          1. delete the prev post please.

            It is part of API:

            app.post(‘/addfrutos’, (req, res) =>{
            const sql = ‘insert into fruto set ?’;
            const customerObj = {
            cod_fruto: req.body.cod_fruto,
            cod_planta: req.body.cod_planta,
            descripcion: req.body.descripcion,
            fecha_recoleccion: req.body.fecha_recoleccion,
            peso: req.body.peso
            }

            connection.query(sql, customerObj, error =>{
            if (error) throw error;
            res.send(‘Fruit created!’);

            });

            });

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like