On this page
You've used generics since your first List<String>. Writing your own is a different matter: why can't you pass a List<Dog> where a MutableList<Animal> is expected? Why does T::class fail inside a function? This guide answers those questions with code you'd actually write in an Android app.
Type parameters and constraints
sealed interface Outcome<out T> {
data class Success<T>(val value: T) : Outcome<T>
data class Failure(val error: Throwable) : Outcome<Nothing>
}
inline fun <T, R> Outcome<T>.map(transform: (T) -> R): Outcome<R> = when (this) {
is Outcome.Success -> Outcome.Success(transform(value))
is Outcome.Failure -> this
}
suspend fun <T> runCatchingIo(block: suspend () -> T): Outcome<T> =
try { Outcome.Success(block()) } catch (e: IOException) { Outcome.Failure(e) }Failure uses Nothing, the type with no values and a subtype of everything. Thanks to out T (covariance, explained next), an Outcome<Nothing> is usable as an Outcome<User>.
fun <T : Comparable<T>> List<T>.secondLargest(): T? = sortedDescending().getOrNull(1)
// Multiple bounds with where
fun <T> persist(item: T) where T : Parcelable, T : Identifiable { /* ... */ }Variance: in and out
Variance answers a question: if Dog is a subtype of Animal, is Box<Dog> a subtype of Box<Animal>? It depends on whether the box produces or consumes values.
MutableList does both, so it is invariant: MutableList<Dog> is not a MutableList<Animal>.val dogs: MutableList<Dog> = mutableListOf(Dog())
// val animals: MutableList<Animal> = dogs // โ compile error, and here's why:
// animals.add(Cat()) // would put a Cat into a list of Dogs!
val readOnly: List<Animal> = dogs // โ
List<out E> only produces: safeKotlin uses declaration-site variance (interface Source<out T>). You can also use use-site variance (fun copy(from: Array<out Any>, to: Array<Any>)) for classes you don't control.
Star projection
List<*> means "a list of some specific but unknown type". You can read elements as Any? but can't add anything. It's useful when you only need type-independent operations, such as size.
Type erasure
On the JVM and Android, generic type arguments are erased at runtime: a List<String> and a List<Int> are both just List. So this doesn't compile:
fun <T> isOfType(value: Any): Boolean = value is T // โ Cannot check for instance of erased type: TReified type parameters
In an inline function, the compiler copies the body to each call site, where the concrete type is known. Mark the parameter reified and you can use T at runtime:
inline fun <reified T> Any.isOfType(): Boolean = this is T
// Start an Activity without passing ::class.java
inline fun <reified A : Activity> Context.startActivity(configure: Intent.() -> Unit = {}) {
startActivity(Intent(this, A::class.java).apply(configure))
}
// context.startActivity<SettingsActivity> { putExtra("tab", "privacy") }
// Filter a mixed feed by type
inline fun <reified T> List<FeedItem>.only(): List<T> = filterIsInstance<T>()
// Decode JSON without passing a serializer explicitly (kotlinx.serialization)
inline fun <reified T> String.decode(json: Json = Json { ignoreUnknownKeys = true }): T =
json.decodeFromString<T>(this)Libraries use this widely: Hilt's hiltViewModel<T>(), Koin's get<T>() and kotlinx.serialization's decodeFromString<T>() all rely on reified generics.
A practical generic: an in-memory cache
class TimedCache<K : Any, V : Any>(
private val ttlMillis: Long,
private val now: () -> Long = { System.currentTimeMillis() },
) {
private data class Entry<V>(val value: V, val storedAt: Long)
private val map = mutableMapOf<K, Entry<V>>()
private val mutex = Mutex()
suspend fun getOrLoad(key: K, load: suspend (K) -> V): V = mutex.withLock {
map[key]?.takeIf { now() - it.storedAt < ttlMillis }?.value
?: load(key).also { map[key] = Entry(it, now()) }
}
}
// val profiles = TimedCache<String, Profile>(ttlMillis = 60_000)
// profiles.getOrLoad(userId) { api.getProfile(it) }Holding the mutex during load keeps it simple but serialises loads. For high concurrency you'd lock per key.
Common mistakes
- Over-generalising: a
BaseRepository<T, ID, DTO, ENTITY>that every feature fights against. Generic code should remove duplication, not create puzzles. - Using
Anyand casts instead of type parameters. - Forgetting that
reifiedrequiresinline, and that inline functions can't be called from Java with the reified behaviour. - Unchecked casts (
as List<User>) that hide erasure problems until runtime.
Frequently asked questions
What's the difference between T, T? and T : Any?
A type parameter T may itself be nullable unless bounded by T : Any. T? explicitly allows null. Use T : Any when you need non-null values.
Is Kotlin's out the same as Java's ? extends?
Yes in meaning. out T corresponds to ? extends T, and in T to ? super T. Kotlin adds declaration-site variance so you rarely need wildcards.
Do reified functions increase app size?
Inline functions duplicate their body at each call site, so keep them small. R8 also optimises in release builds.
Conclusion
Generics give you reusable, type-safe code. Variance (out for producers, in for consumers) explains which substitutions are safe. Erasure removes type arguments at runtime, and reified parameters in inline functions bring them back where you need them.
Next step: reuse behaviour without inheritance using Kotlin Delegation and Property Delegates.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Kotlin fluency makes everything else in Android easier, from coroutines and Flow to Compose and clean architecture. The fastest way to build that fluency is to write real Kotlin every day and get feedback on it.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.