Using Jetpack Compose with Retrofit

In modern Android development, building network-driven applications often requires combining powerful tools and libraries. Two of the most prominent choices are Jetpack Compose for building user interfaces and Retrofit for handling network requests. Using Jetpack Compose with Retrofit can greatly simplify and streamline the process of creating dynamic, data-driven UIs. In this blog post, we’ll explore how to effectively integrate these two libraries to build a robust and efficient Android application.

Introduction to Jetpack Compose

Jetpack Compose is Android’s modern UI toolkit for building native UI. It simplifies and accelerates UI development by providing declarative, composable functions that describe how the UI should look based on its current state. Key features include:

  • Declarative UI: Describes UI as a function of its state.
  • Composable Functions: Reusable UI components.
  • Kotlin-First: Leverages the power of Kotlin for concise and readable code.
  • State Management: Integrated support for managing UI state.

Introduction to Retrofit

Retrofit is a type-safe HTTP client for Android and Java, developed by Square. It simplifies making network requests by converting API endpoints into manageable Java interfaces. Retrofit excels at:

  • Type Safety: Uses Java interfaces to define API endpoints, reducing errors.
  • Easy Integration: Works seamlessly with JSON converters like Gson and Moshi.
  • Annotation-Based: Simplifies request configuration using annotations.
  • Extensibility: Supports custom converters and call adapters.

Setting Up Your Project

Before diving into the integration, set up a new or existing Android project with the necessary dependencies.

Step 1: Add Dependencies

Add the following dependencies to your app-level build.gradle file:


dependencies {
    implementation("androidx.compose.ui:ui:1.6.1")
    implementation("androidx.compose.material:material:1.6.1")
    implementation("androidx.compose.ui:ui-tooling-preview:1.6.1")
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
    implementation("androidx.activity:activity-compose:1.8.2")

    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")

    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

    // Coil for Image Loading
    implementation("io.coil-kt:coil-compose:2.4.0")
}

Step 2: Enable Compose

Ensure that Compose is enabled in your build.gradle file:


android {
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.1" // or newer
    }
}

Creating the API Interface with Retrofit

Define the API endpoints using a Java or Kotlin interface. Let’s create a simple example that fetches a list of posts from a JSONPlaceholder API.

Step 1: Define Data Model

Create a data class that represents a post:


data class Post(
    val userId: Int,
    val id: Int,
    val title: String,
    val body: String
)

Step 2: Create the API Interface

Define the API interface with Retrofit annotations:


import retrofit2.http.GET
import retrofit2.http.Path

interface ApiService {
    @GET("posts")
    suspend fun getPosts(): List<Post>

    @GET("posts/{id}")
    suspend fun getPost(@Path("id") id: Int): Post
}

Step 3: Create Retrofit Instance

Create a singleton object to manage the Retrofit instance:


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

object RetrofitInstance {
    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"

    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }

    private val client = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .build()

    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build()
    }

    val apiService: ApiService by lazy {
        retrofit.create(ApiService::class.java)
    }
}

Fetching Data and Handling State in Jetpack Compose

Now that we have Retrofit set up, let’s integrate it with Jetpack Compose to fetch and display data.

Step 1: Create a ViewModel

Use a ViewModel to manage the UI state and fetch data from the API:


import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {
    private val _posts = MutableStateFlow<List<Post>>(emptyList())
    val posts: StateFlow<List<Post>> = _posts

    private val _errorMessage = MutableStateFlow<String?>(null)
    val errorMessage: StateFlow<String?> = _errorMessage

    init {
        fetchPosts()
    }

    fun fetchPosts() {
        viewModelScope.launch {
            try {
                val fetchedPosts = RetrofitInstance.apiService.getPosts()
                _posts.value = fetchedPosts
                _errorMessage.value = null
            } catch (e: Exception) {
                _posts.value = emptyList()
                _errorMessage.value = "Error fetching posts: ${e.message}"
            }
        }
    }
}

Step 2: Display Data in Compose UI

Create a composable function that observes the state from the ViewModel and displays the posts:


import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun PostListScreen(viewModel: MainViewModel = viewModel()) {
    val posts by viewModel.posts.collectAsState()
    val errorMessage by viewModel.errorMessage.collectAsState()

    MaterialTheme {
        Column {
            if (errorMessage != null) {
                Text(
                    text = "Error: ${errorMessage!!}",
                    color = MaterialTheme.colors.error,
                    modifier = Modifier.padding(16.dp)
                )
            } else {
                LazyColumn(
                    contentPadding = PaddingValues(16.dp)
                ) {
                    items(posts) { post ->
                        PostItem(post = post)
                    }
                }
            }
        }
    }
}

@Composable
fun PostItem(post: Post) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(bottom = 8.dp),
        elevation = 4.dp
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(text = "Title: ${post.title}", style = MaterialTheme.typography.h6)
            Text(text = "Body: ${post.body}", style = MaterialTheme.typography.body2)
        }
    }
}

Step 3: Set Up the Main Activity

In your MainActivity, set the content to your composable:


import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PostListScreen()
        }
    }
}

Handling Errors and Loading States

Effective error handling and loading states are critical for a smooth user experience.

Displaying Loading States

Modify your MainViewModel to include a loading state:


import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {
    private val _posts = MutableStateFlow<List<Post>>(emptyList())
    val posts: StateFlow<List<Post>> = _posts

    private val _errorMessage = MutableStateFlow<String?>(null)
    val errorMessage: StateFlow<String?> = _errorMessage

    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading

    init {
        fetchPosts()
    }

    fun fetchPosts() {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val fetchedPosts = RetrofitInstance.apiService.getPosts()
                _posts.value = fetchedPosts
                _errorMessage.value = null
            } catch (e: Exception) {
                _posts.value = emptyList()
                _errorMessage.value = "Error fetching posts: ${e.message}"
            } finally {
                _isLoading.value = false
            }
        }
    }
}

Update UI for Loading State

Update the PostListScreen composable:


import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun PostListScreen(viewModel: MainViewModel = viewModel()) {
    val posts by viewModel.posts.collectAsState()
    val errorMessage by viewModel.errorMessage.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()

    MaterialTheme {
        Column {
            if (isLoading) {
                Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                    CircularProgressIndicator()
                }
            } else if (errorMessage != null) {
                Text(
                    text = "Error: ${errorMessage!!}",
                    color = MaterialTheme.colors.error,
                    modifier = Modifier.padding(16.dp)
                )
            } else {
                LazyColumn(
                    contentPadding = PaddingValues(16.dp)
                ) {
                    items(posts) { post ->
                        PostItem(post = post)
                    }
                }
            }
        }
    }
}

Conclusion

Integrating Jetpack Compose with Retrofit provides a powerful and efficient approach to building modern Android applications. By using Compose for the UI and Retrofit for networking, you can create highly dynamic and responsive UIs with less boilerplate code. This integration enhances both developer productivity and the user experience. From setting up dependencies to handling errors and loading states, we’ve covered the essential steps to get you started with combining Jetpack Compose and Retrofit in your Android projects. This streamlined process empowers developers to build sophisticated and visually appealing applications with ease.