REST
Retrofit is a type-safe HTTP client for Android and Kotlin that simplifies calling REST APIs by automatically converting JSON to Kotlin/Java objects, handling network requests, and integrating with converters like Moshi.
Android developers use it to avoid writing low-level networking code with HttpUrlConnection or OkHttp manually.
To use Retrofit, you define a data model matching the API JSON, create an interface with annotated HTTP methods (@GET, @POST, etc.), and build a Retrofit instance with a base URL and converter; then you call the API through this interface.
1import com.squareup.moshi.JsonClass
2import java.util.UUID
3
4@JsonClass(generateAdapter = true)
5data class Todo(
6 val id: UUID,
7 val description: String
8)
9
10@JsonClass(generateAdapter = true)
11data class CreateTodoRequest(
12 val description: String
13) 1import com.squareup.moshi.Moshi
2import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
3import retrofit2.Retrofit
4import retrofit2.converter.moshi.MoshiConverterFactory
5import retrofit2.http.*
6import java.util.UUID
7
8interface TodoApi {
9
10 @GET("/api/todos")
11 suspend fun getTodos(): List<Todo>
12
13 @POST("/api/todos")
14 suspend fun createTodo(@Body request: CreateTodoRequest): Todo
15
16 @PUT("/api/todos/{id}")
17 suspend fun updateTodo(@Path("id") id: UUID, @Body request: CreateTodoRequest): Todo
18
19 @DELETE("/api/todos/{id}")
20 suspend fun deleteTodo(@Path("id") id: UUID)
21}
22
23object RetrofitProvider {
24 private const val BASE_URL = "http://10.0.2.2:8080/"
25
26 val moshi = Moshi.Builder()
27 .add(UUID::class.java, UUIDAdapter())
28 .addLast(KotlinJsonAdapterFactory())
29 .build()
30
31 val api: TodoApi by lazy {
32 Retrofit.Builder()
33 .baseUrl(BASE_URL)
34 .addConverterFactory(MoshiConverterFactory.create(moshi))
35 // .addConverterFactory(MoshiConverterFactory.create())
36 .build()
37 .create(TodoApi::class.java)
38 }
39}|
Note
|
In the emulator, the real localhost of your development machine is mapped to 10.0.2.2 because 127.0.0.1 would refer to the emulator itself, not your host computer; this is a special alias provided by the Android emulator to access the host’s network services. |
You may need to write an adapter for some platform classes because Moshi (and other JSON converters) don’t know how to serialize or deserialize these by default.
1import com.squareup.moshi.JsonAdapter
2import java.util.UUID
3
4class UUIDAdapter : JsonAdapter<UUID>() {
5
6 override fun fromJson(reader: com.squareup.moshi.JsonReader): UUID? {
7 val string = reader.nextString()
8 return UUID.fromString(string)
9 }
10
11 override fun toJson(writer: com.squareup.moshi.JsonWriter, value: UUID?) {
12 writer.value(value?.toString())
13 }
14
15}Coroutines are Kotlin’s way of writing asynchronous, non-blocking code sequentially.
viewModelScope is a coroutine scope tied to a ViewModel that automatically cancels its coroutines when the ViewModel is cleared.
Data fetching should be done inside the ViewModel, keeping Composables stateless and declarative; use LaunchedEffect in a Composable only for one-off side effects triggered by composition, like refreshing data when a screen appears.
1import androidx.lifecycle.ViewModel
2import androidx.lifecycle.viewModelScope
3import kotlinx.coroutines.flow.MutableStateFlow
4import kotlinx.coroutines.flow.StateFlow
5import kotlinx.coroutines.launch
6import java.util.UUID
7
8class TodosViewModel : ViewModel() {
9
10 private val api = RetrofitProvider.api
11
12 private val _todos = MutableStateFlow<List<Todo>>(emptyList())
13 val todos: StateFlow<List<Todo>> = _todos
14
15 init {
16 loadTodos()
17 }
18
19 fun loadTodos() {
20 viewModelScope.launch {
21 _todos.value = api.getTodos()
22 }
23 }
24
25 fun createTodo(description: String, onComplete: () -> Unit) {
26 viewModelScope.launch {
27 api.createTodo(CreateTodoRequest(description))
28 loadTodos()
29 onComplete()
30 }
31 }
32
33 fun updateTodo(id: UUID, description: String, onComplete: () -> Unit) {
34 viewModelScope.launch {
35 api.updateTodo(id, CreateTodoRequest(description))
36 loadTodos()
37 onComplete()
38 }
39 }
40
41 fun deleteTodo(id: UUID) {
42 viewModelScope.launch {
43 api.deleteTodo(id)
44 loadTodos()
45 }
46 }
47
48} 1@Composable
2fun TodoDetailScreen(
3 todoIdArg: String,
4 viewModel: TodosViewModel,
5 onDone: () -> Unit
6) {
7 var description by remember { mutableStateOf("") }
8 val isNew = todoIdArg == "new"
9 val todoId = if (isNew) null else UUID.fromString(todoIdArg)
10
11 LaunchedEffect(todoId) {
12 if (!isNew && todoId != null) {
13 val todo = viewModel.todos.value.find { it.id == todoId }
14 description = todo?.description ?: ""
15 }
16 }
17
18 Scaffold { padding ->
19 Column(modifier = Modifier
20 .padding(16.dp)
21 .fillMaxWidth()
22 ) {
23 OutlinedTextField(
24 value = description,
25 onValueChange = { description = it },
26 label = { Text("Description") },
27 modifier = Modifier.fillMaxWidth()
28 )
29 Spacer(modifier = Modifier.height(16.dp))
30 Button(
31 onClick = {
32 if (isNew) {
33 viewModel.createTodo(description, onDone)
34 } else {
35 todoId?.let { viewModel.updateTodo(it, description, onDone) }
36 }
37 },
38 modifier = Modifier.fillMaxWidth()
39 ) {
40 Text(if (isNew) "Create" else "Save")
41 }
42 }
43 }
44}For HTTP calls, you need to add <uses-permission android:name="android.permission.INTERNET"/> to the Android Manifest so your app has permission to access the network, and on Android 9+ you may also need android:usesCleartextTraffic="true" if connecting over plain HTTP instead of HTTPS.
1<?xml version="1.0" encoding="utf-8"?>
2<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools">
4
5 <uses-permission android:name="android.permission.INTERNET" />
6
7 <application
8 android:allowBackup="true"
9 android:dataExtractionRules="@xml/data_extraction_rules"
10 android:fullBackupContent="@xml/backup_rules"
11 android:icon="@mipmap/ic_launcher"
12 android:label="@string/app_name"
13 android:roundIcon="@mipmap/ic_launcher_round"
14 android:supportsRtl="true"
15 android:theme="@style/Theme.TodoClientApp"
16 android:usesCleartextTraffic="true">
17 <activity
18 android:name=".MainActivity"
19 android:exported="true"
20 android:label="@string/app_name"
21 android:theme="@style/Theme.TodoClientApp">
22 <intent-filter>
23 <action android:name="android.intent.action.MAIN" />
24
25 <category android:name="android.intent.category.LAUNCHER" />
26 </intent-filter>
27 </activity>
28 </application>
29
30</manifest>To make this work, you will need to update your libs.versions.toml and build.gradle.kts files.
Version catalog (libs.versions.toml):
1[versions]
2[...]
3moshi = "1.15.2"
4retrofit = "3.0.0"
5ksp = "2.0.21-1.0.25"
6
7[libraries]
8[...]
9converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
10moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
11moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
12moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
13retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
14
15[plugins]
16[...]
17ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }Root build.gradle.kts:
1// Top-level build file where you can add configuration options common to all sub-projects/modules.
2plugins {
3 alias(libs.plugins.android.application) apply false
4 alias(libs.plugins.kotlin.android) apply false
5 alias(libs.plugins.kotlin.compose) apply false
6 alias(libs.plugins.ksp) apply false
7}Module build.gradle.kts:
1plugins {
2 [...]
3 alias(libs.plugins.ksp)
4}
5[...]
6
7dependencies {
8 [...]
9 implementation(libs.retrofit)
10 implementation(libs.converter.moshi)
11 implementation(libs.moshi)
12 implementation(libs.moshi.kotlin)
13 ksp(libs.moshi.kotlin.codegen)
14 [...]
15}