Real World Example

A Generic Resource Wrapper and Repository

Android apps wrap every data load in a generic type (often called Resource<T> or UiState<T>) and use one generic in-memory cache for many entity types.

Example

sealed interface Resource<out T> {
    data object Loading : Resource<Nothing>
    data class Success<T>(val data: T) : Resource<T>
    data class Error(val message: String) : Resource<Nothing>
}

interface Entity { val id: Int }
data class Product(override val id: Int, val name: String) : Entity
data class Category(override val id: Int, val title: String) : Entity

class MemoryRepository<T : Entity> {
    private val store = linkedMapOf<Int, T>()
    fun save(item: T) { store[item.id] = item }
    fun find(id: Int): Resource<T> = store[id]?.let { Resource.Success(it) } ?: Resource.Error("Item $id not found")
    fun all(): Resource<List<T>> = Resource.Success(store.values.toList())
}

fun <T> render(r: Resource<T>) = when (r) {
    Resource.Loading -> "Loading..."
    is Resource.Success -> "OK: ${r.data}"
    is Resource.Error -> "Error: ${r.message}"
}

fun main() {
    val products = MemoryRepository<Product>()
    products.save(Product(1, "Smart Watch"))
    products.save(Product(2, "Earbuds"))
    val categories = MemoryRepository<Category>()
    categories.save(Category(10, "Wearables"))

    println(render(Resource.Loading))
    println(render(products.find(2)))
    println(render(products.find(9)))
    println(render(categories.all()))
}

Output

Loading...
OK: Product(id=2, name=Earbuds)
Error: Item 9 not found
OK: [Category(id=10, title=Wearables)]

Note: Resource<out T> plus Resource<Nothing> lets Loading and Error be used as any Resource<Product> or Resource<List<Category>>, because Nothing is a subtype of every type.

Retrofit Generic Response

@Serializable
data class ApiResponse<T>(val success: Boolean, val data: T?, val message: String? = null)

interface ShopApi {
    @GET("products")
    suspend fun products(): ApiResponse<List<ProductDto>>

    @GET("users/{id}")
    suspend fun user(@Path("id") id: Long): ApiResponse<UserDto>
}