Skip to main content

Use Cases

Use cases encapsulate business logic and orchestrate data flow between repositories and the presentation layer. They represent the application’s core functionality following Clean Architecture principles.

GetProductsUseCase

Manages product search operations with built-in validation logic.

Class Definition

class GetProductsUseCase @Inject constructor(
    private val repository: ProductRepository
) {
    suspend operator fun invoke(query: String): Result<List<Product>>
}

Constructor

repository
ProductRepository
required
Product repository for accessing the data source

Methods

invoke

Executes the product search operation.
suspend operator fun invoke(query: String): Result<List<Product>>
query
String
required
Search term for finding products
returns
Result<List<Product>>
Result containing a list of Product models. Returns an empty list if the query is blank

Business Logic

This use case implements the following validation:
  • If the search query is blank or empty, returns a successful empty list without making a repository call
  • Otherwise, delegates to the repository to perform the search
This prevents unnecessary API calls and provides consistent behavior for empty searches.

Usage Examples

In a ViewModel

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val getProductsUseCase: GetProductsUseCase
) : ViewModel() {
    
    private val _uiState = MutableStateFlow<UiState<List<Product>>>(UiState.Idle)
    val uiState: StateFlow<UiState<List<Product>>> = _uiState.asStateFlow()
    
    fun searchProducts(query: String) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            
            val result = getProductsUseCase(query)
            
            _uiState.value = result.fold(
                onSuccess = { products -> UiState.Success(products) },
                onFailure = { error -> UiState.Error(error.message ?: "Unknown error") }
            )
        }
    }
}

Direct Usage

class ProductSearchService @Inject constructor(
    private val getProductsUseCase: GetProductsUseCase
) {
    suspend fun search(term: String): List<Product> {
        val result = getProductsUseCase(term)
        return result.getOrElse { emptyList() }
    }
}

Handling Results

// Approach 1: Using fold
getProductsUseCase("smartphone").fold(
    onSuccess = { products ->
        println("Found ${products.size} products")
        products.forEach { println(it.title) }
    },
    onFailure = { error ->
        when (error) {
            is AppError.Network -> println("Network error")
            is AppError.Timeout -> println("Request timeout")
            is AppError.Server -> println("Server error: ${error.code}")
            else -> println("Unknown error")
        }
    }
)

// Approach 2: Using getOrNull
val products = getProductsUseCase("laptop").getOrNull()
if (products != null) {
    displayProducts(products)
} else {
    showError()
}

// Approach 3: Using getOrElse
val products = getProductsUseCase("tablet").getOrElse { emptyList() }
displayProducts(products)

GetProductDetailUseCase

Retrieves detailed information for a specific product.

Class Definition

class GetProductDetailUseCase @Inject constructor(
    private val repository: ProductRepository
) {
    suspend operator fun invoke(id: String): Result<ProductDetail>
}

Constructor

repository
ProductRepository
required
Product repository for accessing the data source

Methods

invoke

Executes the product detail query.
suspend operator fun invoke(id: String): Result<ProductDetail>
id
String
required
Unique product identifier (e.g., “MCO12345”)
returns
Result<ProductDetail>
Result containing the ProductDetail domain model

Business Logic

This use case acts as a simple pass-through to the repository, delegating the retrieval operation without additional validation. Future enhancements could include:
  • Caching logic
  • Analytics tracking
  • Related product recommendations
  • Favorite/wishlist integration

Usage Examples

In a ViewModel

@HiltViewModel
class ProductDetailViewModel @Inject constructor(
    private val getProductDetailUseCase: GetProductDetailUseCase,
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    
    private val productId: String = checkNotNull(savedStateHandle["productId"])
    
    private val _uiState = MutableStateFlow<UiState<ProductDetail>>(UiState.Loading)
    val uiState: StateFlow<UiState<ProductDetail>> = _uiState.asStateFlow()
    
    init {
        loadProductDetail()
    }
    
    private fun loadProductDetail() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            
            val result = getProductDetailUseCase(productId)
            
            _uiState.value = result.fold(
                onSuccess = { detail -> UiState.Success(detail) },
                onFailure = { error -> UiState.Error(error.message ?: "Failed to load product") }
            )
        }
    }
    
    fun retry() {
        loadProductDetail()
    }
}

With Error Handling

suspend fun loadProductWithFallback(productId: String): ProductDetail? {
    return getProductDetailUseCase(productId).fold(
        onSuccess = { detail -> detail },
        onFailure = { error ->
            // Log error for analytics
            analyticsService.logError("product_detail_error", mapOf(
                "product_id" to productId,
                "error_type" to error::class.simpleName
            ))
            
            // Return null or throw depending on requirements
            null
        }
    )
}

Combining Multiple Use Cases

class ProductService @Inject constructor(
    private val getProductsUseCase: GetProductsUseCase,
    private val getProductDetailUseCase: GetProductDetailUseCase
) {
    suspend fun searchAndGetFirstDetail(query: String): ProductDetail? {
        // First search for products
        val searchResult = getProductsUseCase(query).getOrNull()
        
        // Get the first product's ID
        val firstProductId = searchResult?.firstOrNull()?.id ?: return null
        
        // Get detailed information
        return getProductDetailUseCase(firstProductId).getOrNull()
    }
}

Dependency Injection

Both use cases are designed to work with Dagger Hilt dependency injection:
@Module
@InstallIn(ViewModelComponent::class)
object UseCaseModule {
    
    @Provides
    fun provideGetProductsUseCase(
        repository: ProductRepository
    ): GetProductsUseCase {
        return GetProductsUseCase(repository)
    }
    
    @Provides
    fun provideGetProductDetailUseCase(
        repository: ProductRepository
    ): GetProductDetailUseCase {
        return GetProductDetailUseCase(repository)
    }
}
However, since both use cases use @Inject constructors, Hilt can automatically provide them without explicit module definitions.

Testing

Use cases are easily testable by mocking the repository dependency:
@Test
fun `searchProducts with blank query returns empty list`() = runTest {
    // Given
    val repository = mockk<ProductRepository>()
    val useCase = GetProductsUseCase(repository)
    
    // When
    val result = useCase("   ")
    
    // Then
    assertTrue(result.isSuccess)
    assertEquals(emptyList<Product>(), result.getOrNull())
    verify(exactly = 0) { repository.searchProducts(any()) }
}

@Test
fun `searchProducts with valid query calls repository`() = runTest {
    // Given
    val mockProducts = listOf(
        Product("1", "Product 1"),
        Product("2", "Product 2")
    )
    val repository = mockk<ProductRepository> {
        coEvery { searchProducts("test") } returns Result.success(mockProducts)
    }
    val useCase = GetProductsUseCase(repository)
    
    // When
    val result = useCase("test")
    
    // Then
    assertTrue(result.isSuccess)
    assertEquals(mockProducts, result.getOrNull())
    coVerify(exactly = 1) { repository.searchProducts("test") }
}

Package Location

com.alcalist.tecmeli.domain.usecase
Use cases are located in the domain.usecase package, interacting with repository interfaces from domain.repository and returning domain models from domain.model.

Build docs developers (and LLMs) love