This guide explains how to integrate a new API call with a new ViewModel in the Compose-Base Android project. The project follows Clean Architecture principles with MVVM pattern, Hilt for dependency injection, and Coroutines Flow for reactive programming.
The project follows this flow:
UI (Activity/Fragment) → ViewModel → UseCase → Repository → DataSource → ApiInterface
First, create a response model for your API in the data/remote/model package.
File: app/src/main/java/com/coreproc/kotlin/kotlinbase/data/remote/model/UserResponse.kt
package com.coreproc.kotlin.kotlinbase.data.remote.model
data class UserResponse(
val id: Int,
val name: String,
val email: String,
val avatar: String?
)Add your new API endpoint to the ApiInterface.
File: app/src/main/java/com/coreproc/kotlin/kotlinbase/data/remote/ApiInterface.kt
interface ApiInterface {
@GET("random_joke")
suspend fun getSomething(): Response<SampleResponse>
// Add your new endpoint
@GET("users/{id}")
suspend fun getUser(@Path("id") userId: Int): Response<UserResponse>
@POST("users")
suspend fun createUser(@Body user: CreateUserRequest): Response<UserResponse>
}Add the new method to the DataSource interface.
File: app/src/main/java/com/coreproc/kotlin/kotlinbase/data/remote/DataSource.kt
interface DataSource {
suspend fun getSomething(): Flow<ResponseHandler<SampleResponse>>
// Add your new methods
suspend fun getUser(userId: Int): Flow<ResponseHandler<UserResponse>>
suspend fun createUser(user: CreateUserRequest): Flow<ResponseHandler<UserResponse>>
}Implement the new methods in the DataRepository.
File: app/src/main/java/com/coreproc/kotlin/kotlinbase/data/remote/DataRepository.kt
class DataRepository
@Inject
constructor(
private val apiInterface: ApiInterface
) : DataSource {
override suspend fun getSomething(): Flow<ResponseHandler<SampleResponse>> {
// ...existing implementation...
}
override suspend fun getUser(userId: Int): Flow<ResponseHandler<UserResponse>> {
return handleApiCall(
apiCall = { apiInterface.getUser(userId) },
onSuccess = { user ->
Timber.d("Successfully fetched user: ${user.name}")
// Add any custom logic here:
// - Save to local database
// - Update cache
// - Perform validations
}
)
}
override suspend fun createUser(user: CreateUserRequest): Flow<ResponseHandler<UserResponse>> {
return handleApiCall(
apiCall = { apiInterface.createUser(user) },
onSuccess = { createdUser ->
Timber.d("Successfully created user: ${createdUser.name}")
// Custom logic for user creation
}
)
}
}Create a UseCase for your new API call.
File: app/src/main/java/com/coreproc/kotlin/kotlinbase/data/remote/usecase/UserUseCase.kt
package com.coreproc.kotlin.kotlinbase.data.remote.usecase
import com.coreproc.kotlin.kotlinbase.data.remote.DataSource
import com.coreproc.kotlin.kotlinbase.data.remote.ResponseHandler
import com.coreproc.kotlin.kotlinbase.data.remote.UseCase
import com.coreproc.kotlin.kotlinbase.data.remote.model.UserResponse
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetUserUseCase
@Inject
constructor(private val dataSource: DataSource) : UseCase<Int, Flow<ResponseHandler<UserResponse>>>() {
override suspend fun executeUseCase(requestValues: Int): Flow<ResponseHandler<UserResponse>> {
return dataSource.getUser(requestValues)
}
}
class CreateUserUseCase
@Inject
constructor(private val dataSource: DataSource) : UseCase<CreateUserRequest, Flow<ResponseHandler<UserResponse>>>() {
override suspend fun executeUseCase(requestValues: CreateUserRequest): Flow<ResponseHandler<UserResponse>> {
return dataSource.createUser(requestValues)
}
}Create your new ViewModel extending BaseViewModel.
File: app/src/main/java/com/coreproc/kotlin/kotlinbase/ui/user/UserViewModel.kt
package com.coreproc.kotlin.kotlinbase.ui.user
import androidx.lifecycle.viewModelScope
import com.coreproc.kotlin.kotlinbase.data.remote.model.UserResponse
import com.coreproc.kotlin.kotlinbase.data.remote.usecase.GetUserUseCase
import com.coreproc.kotlin.kotlinbase.data.remote.usecase.CreateUserUseCase
import com.coreproc.kotlin.kotlinbase.extensions.handleResponse
import com.coreproc.kotlin.kotlinbase.ui.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class UserViewModel
@Inject
constructor(
private val getUserUseCase: GetUserUseCase,
private val createUserUseCase: CreateUserUseCase
) : BaseViewModel() {
// State for user data
private val _userState = MutableStateFlow<UserResponse?>(null)
val userState: StateFlow<UserResponse?> = _userState.asStateFlow()
// State for user creation success
private val _userCreated = MutableStateFlow(false)
val userCreated: StateFlow<Boolean> = _userCreated.asStateFlow()
fun getUser(userId: Int) = viewModelScope.launch(Dispatchers.IO) {
getUserUseCase.run(userId)
.collectLatest { response ->
response.handleResponse(this@UserViewModel) { user ->
_userState.value = user
}
}
}
fun createUser(createUserRequest: CreateUserRequest) = viewModelScope.launch(Dispatchers.IO) {
createUserUseCase.run(createUserRequest)
.collectLatest { response ->
response.handleResponse(this@UserViewModel) { user ->
_userState.value = user
_userCreated.value = true
}
}
}
// Manual response handling example (alternative to handleResponse extension)
fun getUserManual(userId: Int) = viewModelScope.launch(Dispatchers.IO) {
getUserUseCase.run(userId)
.collectLatest { response ->
when (response) {
is ResponseHandler.Loading -> {
setLoading(response.loading)
}
is ResponseHandler.Error -> {
setError(response.errorBody!!)
}
is ResponseHandler.Failure -> {
setFailure(response.exception ?: Throwable("Unknown error"))
}
is ResponseHandler.Success -> {
setLoading(false)
response.result?.let {
_userState.value = it
}
}
}
}
}
fun clearUserCreatedState() {
_userCreated.value = false
}
}Create your UI component that uses the ViewModel.
File: app/src/main/java/com/coreproc/kotlin/kotlinbase/ui/user/UserFragment.kt
package com.coreproc.kotlin.kotlinbase.ui.user
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.coreproc.kotlin.kotlinbase.databinding.FragmentUserBinding
import com.coreproc.kotlin.kotlinbase.ui.base.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class UserFragment : BaseFragment() {
private val viewModel: UserViewModel by viewModels()
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): ViewBinding {
return FragmentUserBinding.inflate(inflater, container, false)
}
override fun initialize() {
val binding = getBinding<FragmentUserBinding>()
setupViews(binding)
setupObservers(binding)
setupListeners(binding)
}
private fun setupViews(binding: FragmentUserBinding) {
// Initialize your views
binding.buttonGetUser.text = "Get User"
}
private fun setupObservers(binding: FragmentUserBinding) {
lifecycleScope.launch {
// Observe loading state
viewModel.loadingStateFlow.collect { isLoading ->
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}
}
lifecycleScope.launch {
// Observe user data
viewModel.userState.collect { user ->
user?.let {
binding.textViewUserName.text = it.name
binding.textViewUserEmail.text = it.email
}
}
}
lifecycleScope.launch {
// Observe user creation state
viewModel.userCreated.collect { created ->
if (created) {
// Handle user creation success
showToast("User created successfully!")
viewModel.clearUserCreatedState()
}
}
}
lifecycleScope.launch {
// Observe errors
viewModel.errorStateFlow.collect { error ->
error?.let {
showErrorDialog(it.message ?: "An error occurred")
viewModel.clearError()
}
}
}
}
private fun setupListeners(binding: FragmentUserBinding) {
binding.buttonGetUser.setOnClickListener {
viewModel.getUser(1) // Get user with ID 1
}
binding.buttonCreateUser.setOnClickListener {
val createRequest = CreateUserRequest(
name = binding.editTextName.text.toString(),
email = binding.editTextEmail.text.toString()
)
viewModel.createUser(createRequest)
}
}
}If using Jetpack Compose, here's how to implement the UI:
File: app/src/main/java/com/coreproc/kotlin/kotlinbase/ui/user/UserScreen.kt
package com.coreproc.kotlin.kotlinbase.ui.user
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun UserScreen(
viewModel: UserViewModel = hiltViewModel()
) {
val userState by viewModel.userState.collectAsStateWithLifecycle()
val isLoading by viewModel.loadingStateFlow.collectAsStateWithLifecycle()
val error by viewModel.errorStateFlow.collectAsStateWithLifecycle()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isLoading) {
CircularProgressIndicator()
}
userState?.let { user ->
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = "Name: ${user.name}")
Text(text = "Email: ${user.email}")
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { viewModel.getUser(1) }
) {
Text("Get User")
}
error?.let { errorBody ->
LaunchedEffect(errorBody) {
// Handle error (show snackbar, etc.)
viewModel.clearError()
}
}
}
}The project uses Hilt, so most dependencies are automatically injected. However, if you need custom binding:
File: app/src/main/java/com/coreproc/kotlin/kotlinbase/di/module/UseCaseModule.kt
@InstallIn(SingletonComponent::class)
@Module
class UseCaseModule {
@Provides
@Singleton
fun provideGetUserUseCase(dataSource: DataSource): GetUserUseCase {
return GetUserUseCase(dataSource)
}
}- Use
handleResponseextension for automatic error handling - Override base error handling when custom logic is needed
- Always clear error states after handling them
- Loading is automatically managed by
ResponseHandler - BaseViewModel provides
loadingStateFlowfor UI updates
- Use
StateFlowfor reactive state management - Follow the pattern: private
MutableStateFlowwith publicStateFlow - Use
collectAsStateWithLifecycle()in Compose for automatic lifecycle handling
- Repository and UseCase operations run on
Dispatchers.IO - UI updates are automatically dispatched to the main thread
- Mock the DataSource for ViewModel testing
- Mock the ApiInterface for Repository testing
- Use
TestDispatcherfor coroutine testing
fun loadUserWithPosts(userId: Int) = viewModelScope.launch {
// Load user and posts concurrently
val userDeferred = async { getUserUseCase.run(userId) }
val postsDeferred = async { getUserPostsUseCase.run(userId) }
// Collect both flows
launch { userDeferred.await().collectLatest { /* handle user */ } }
launch { postsDeferred.await().collectLatest { /* handle posts */ } }
}fun loadUserData(userId: Int) = viewModelScope.launch {
getUserUseCase.run(userId)
.collectLatest { response ->
response.handleResponse(this@UserViewModel) { user ->
_userState.value = user
// Load additional data based on user type
if (user.isPremium) {
loadPremiumFeatures()
}
}
}
}This guide covers the complete flow for integrating new API calls with ViewModels in the Compose-Base project. Follow this pattern consistently for all new API integrations.