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:

  1. Create your data model

  2. Define a Retrofit interface

  3. Create a repository extending BaseRepository

  4. Create a ViewModel extending ApiUiViewModel

  5. 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

Popular posts from this blog

Jetpack Compose based Android interview and questions

Kotlin Some important unsorted interview questions and answers

Null safety based Kotlin interview questions and answers