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 .
Table of Contents
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
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!’);
});
});
Then, obviously, the JSON format of the response body is incorrect
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
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.
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.
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)
The last sentence:
RETURN: Fruit created
Is it a part of JSON?
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!’);
});
});