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>>
Search term for finding products
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>
Unique product identifier (e.g., “MCO12345”)
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.