Chapter 15 - Coding the SearchImageUseCase (Domain layer)
Before we start working on the same blueprint we used earlier for the GetArtUseCase, let us pause for a moment and depict the data flow in the form of a flowchart. This will help gauge the progress of the second use case - SearchImageUseCase. This use case revolves around fetching images from the Remote API after user types in a “search” query string inside the search window.
DATA FLOW DIAGRAM
Domain layer defines business logic and repository contracts.
Data layer implements actual API calls (Retrofit) and maps DTOs → Domain Models.
Hilt provides singletons of API, Repository, and UseCases for dependency injection.
ViewModel consumes UseCases and exposes state to UI via StateFlow.
Composables observe ViewModel state and render UI accordingly.
Clean flow: UI → ViewModel → UseCase → Repository → API → Mapper → Domain Model.
Coding the Domain Model
The ImageResult class
package com.learning.artsnapapp.domain.model
data class ImageResult (
val id: Int,
val previewURL: String,
val largeImageURL: String
)
The Repository Interface
Let us check the repository interface that we used for our first use case.
package com.learning.artsnapapp.domain.repository
import com.learning.artsnapapp.domain.model.Art
import com.learning.artsnapapp.domain.model.ImageResult
import com.learning.artsnapapp.util.Resource
import kotlinx.coroutines.flow.Flow
interface ArtRepositoryInterface {
suspend fun insertArt(art: Art)
suspend fun deleteArt(art: Art)
fun getArt(): Flow<List<Art>>
fun searchImage(imageString: String): Flow<Resource<List<ImageResult>>>
}
/**
The domain layer defines what data is needed, not how it is fetched.
The data layer will implement this interface.
* */
We already defined the searchImage function that will return Flow of the List of ImageResult objects. This is similar to the getArt function returning the List of Art objects that we have already worked on in the previous chapters.
We are walking on the already trodden path and things will be simpler to understand this time around. The power of clarity that the Clean Architecture provides will be seen here. We have already worked on the Hilt dependency injection and hence this time we will re-use what is already done to build this SearchImageUseCase functionality quickly.
Here is the use case code
package com.learning.artsnapapp.domain.usecase
import com.learning.artsnapapp.domain.model.ImageResult
import com.learning.artsnapapp.domain.repository.ArtRepositoryInterface
import com.learning.artsnapapp.util.Resource
import kotlinx.coroutines.flow.Flow
class SearchImageUseCase(private val imageRepository: ArtRepositoryInterface)
{
operator fun invoke(searchQuery:String) : Flow<Resource<List<ImageResult>>> {
return imageRepository.searchImage(searchQuery)
}
}
//Purpose: Search for artwork images online using the external remote API.
//Domain logic: Wrap API responses into Resource and map DTO → domain model.
We need to understand the Resource utility class. There is a separate package “util” to handle utility classes.
The RESOURCE class
package com.learning.artsnapapp.util
sealed class Resource<T>(
val data: T? = null,
val message: String? = null
) {
class Success<T>(data: T) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
class Loading<T>(data: T? = null) : Resource<T>(data)
companion object {
fun <T> success(data: T): Resource<T> = Success(data)
fun <T> error(msg: String, data: T? = null): Resource<T> = Error(msg, data)
fun <T> loading(data: T? = null): Resource<T> = Loading(data)
}
}
Resource - A wrapper class that represents Loading, Success, or Error for any data type. To make UI and ViewModel communication cleaner and handle async states elegantly.
Resource is not about data, it’s about data state.
It helps ViewModels and Composables talk in the same structured language
Benefits - Predictable state management, no null-check hell, smoother Compose UI updates.
KOTLIN SEALED CLASS AND BENEFITS
A sealed class in Kotlin is a restricted class hierarchy — it allows you to define a fixed set of subclasses that represent different variants of a concept.
You can think of it like a closed family of related classes. Only the types you declare inside it are allowed to exist — nothing else can extend it.
Why Use Sealed Classes?
Type-safe state management You can handle different outcomes (like Success, Error, Loading) clearly.
No magic numbers or enums Instead of using strings or codes (“STATUS=200”), each state is an actual class.
Smart compiler help If you forget to handle a state in a when block, the compiler warns you
In the next chapter, we will code the data layer for the SearchImageUseCase.