A Reusable MVVM API Call Pattern in Jetpack Compose
Building scalable Android apps requires clean architecture—and one of the best ways to manage your UI and logic is by combining MVVM with Jetpack Compose. However, API calls can get messy when not structured properly.
In this post, I’ll show you how to build a reusable, clean API call structure using MVVM that works for any screen in your app. We'll use:
-
ViewModel to manage UI-related data
-
Repository for business logic and network
-
State management using
mutableStateOf
-
Retrofit for networking
This is not tied to any specific screen (like sliders, onboarding, etc.)—you can plug in your own model and API.
Why a Reusable Pattern?
Apps often repeat the same logic:
-
Show loader
-
Fetch data
-
Handle errors
-
Update UI
Instead of repeating it everywhere, we'll abstract it once and just extend or plug in what’s needed per screen.
1. Define a Generic API Result Wrapper
This wrapper lets us handle success, loading, and error states in a consistent way.
sealed class ApiResult<out T> {
object Loading : ApiResult<Nothing>()
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val message: String) : ApiResult<Nothing>()
}
2. Create a Base ViewModel
This helps manage shared UI states like loading and error.
open class ApiUiViewModel : ViewModel() {
var isLoading by mutableStateOf(false)
protected set
var errorMessage by mutableStateOf<String?>(null)
protected set
protected fun <T> handleApiResult(result: ApiResult<T>, onSuccess: (T) -> Unit) {
when (result) {
is ApiResult.Loading -> {
isLoading = true
errorMessage = null
}
is ApiResult.Success -> {
isLoading = false
onSuccess(result.data)
}
is ApiResult.Error -> {
isLoading = false
errorMessage = result.message
}
}
}
}
3. Retrofit Setup
Just the usual Retrofit setup:
object ApiClient {
val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl("https://your.api.url/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
4. Base Repository for Safe API Calls
This helps catch exceptions and HTTP errors cleanly.
open class BaseRepository {
suspend fun <T> safeApiCall(apiCall: suspend () -> Response<T>): ApiResult<T> {
return try {
val response = apiCall()
if (response.isSuccessful && response.body() != null) {
ApiResult.Success(response.body()!!)
} else {
ApiResult.Error("Error: ${response.code()} ${response.message()}")
}
} catch (e: Exception) {
ApiResult.Error("Exception: ${e.localizedMessage ?: "Unknown error"}")
}
}
}
5. Example Repository
Here's an example repository for sliders (replace with your own data model and API).
class SliderRepository : BaseRepository() {
private val api = ApiClient.retrofit.create(SliderApi::class.java)
suspend fun getSliderItems(): ApiResult<List<SliderItem>> {
return safeApiCall { api.getSliders() }
}
}
6. Example ViewModel
Just extend the base ApiUiViewModel
and plug in your repository.
class SliderViewModel(
private val repository: SliderRepository = SliderRepository()
) : ApiUiViewModel() {
var sliders by mutableStateOf<List<SliderItem>>(emptyList())
private set
init {
loadSliders()
}
fun loadSliders() {
viewModelScope.launch {
handleApiResult(repository.getSliderItems()) {
sliders = it
}
}
}
}
7. Jetpack Compose UI Example
Observe the ViewModel and show data or errors in the UI.
@Composable
fun SliderScreen(viewModel: SliderViewModel = viewModel()) {
val sliders = viewModel.sliders
val isLoading = viewModel.isLoading
val error = viewModel.errorMessage
when {
isLoading -> CircularProgressIndicator()
error != null -> Text("Error: $error")
else -> LazyRow {
items(sliders) { slider ->
Column {
Text(slider.title)
Text(slider.description)
}
}
}
}
}
To Reuse This Setup
Whenever you build a new feature:
-
Create your data model
-
Define a Retrofit interface
-
Create a repository extending
BaseRepository
-
Create a ViewModel extending
ApiUiViewModel
-
Use the ViewModel in your Compose screen
That’s it!
Final Thoughts
This pattern simplifies scaling your app:
-
Reduces boilerplate
-
Encourages separation of concerns
-
Makes testing and debugging easier
Start with one feature, and you’ll soon have a clean, maintainable app architecture.
Comments
Post a Comment