Skip to content

Latest commit

 

History

History
472 lines (380 loc) · 14.5 KB

File metadata and controls

472 lines (380 loc) · 14.5 KB

API Integration Guide with ViewModel

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.

Architecture Overview

The project follows this flow:

UI (Activity/Fragment) → ViewModel → UseCase → Repository → DataSource → ApiInterface

Step-by-Step Integration

1. Create the Data Model

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?
)

2. Update ApiInterface

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>
}

3. Update DataSource Interface

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>>
}

4. Update DataRepository

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
            }
        )
    }
}

5. Create UseCase

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)
    }
}

6. Create ViewModel

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
    }
}

7. Create UI Component (Fragment/Activity)

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)
        }
    }
}

8. Compose UI Example

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()
            }
        }
    }
}

9. Update DI Modules (if needed)

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)
    }
}

Key Points to Remember

1. Error Handling

  • Use handleResponse extension for automatic error handling
  • Override base error handling when custom logic is needed
  • Always clear error states after handling them

2. Loading States

  • Loading is automatically managed by ResponseHandler
  • BaseViewModel provides loadingStateFlow for UI updates

3. State Management

  • Use StateFlow for reactive state management
  • Follow the pattern: private MutableStateFlow with public StateFlow
  • Use collectAsStateWithLifecycle() in Compose for automatic lifecycle handling

4. Threading

  • Repository and UseCase operations run on Dispatchers.IO
  • UI updates are automatically dispatched to the main thread

5. Testing

  • Mock the DataSource for ViewModel testing
  • Mock the ApiInterface for Repository testing
  • Use TestDispatcher for coroutine testing

Common Patterns

Multiple API Calls

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 */ } }
}

Conditional API Calls

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.