What Are Kotlin Generics and Why They Matter
Generics are one of Kotlin's most powerful features for building type-safe, reusable code across your mobile applications. Whether you're developing native Android apps with Kotlin, building cross-platform solutions with Kotlin Multiplatform, or working with code shared between iOS and Android, understanding generics is essential for creating robust, maintainable mobile applications.
At its core, generics allow you to write flexible, reusable code that works with different types while maintaining compile-time type safety. Instead of writing separate code for each data type you need to handle, generics let you write one implementation that works with any type you specify. This approach eliminates the need for error-prone type casting and reduces boilerplate code throughout your mobile projects.
The power of generics becomes particularly evident in mobile development, where you frequently work with collections of data, network responses, and UI components that need to display various types of content. A well-designed generic repository can handle user data, product information, and any other entity your app needs, all with the same type-safe implementation. Similarly, generic RecyclerView adapters can render any data type in your lists, making your UI code more maintainable and less repetitive.
Key benefits covered:
- Compile-time type safety that catches errors before runtime
- Code reusability across multiple data types
- Elimination of explicit type casting and ClassCastException risks
- Powerful abstractions for data layers and UI components
For mobile developers specifically, generics appear throughout the Kotlin ecosystem. The standard library's collections (List, Set, Map) all use generics. Retrofit's type-safe HTTP client relies on generics. Room's database access objects leverage generics. Jetpack Compose's components use generics extensively. By understanding generics deeply, you unlock the full potential of these tools and can build more sophisticated features with confidence.
If you're building Android applications, mastering generics is a fundamental skill that will improve code quality across your entire codebase.
Understanding these core advantages helps you apply generics effectively
Compile-Time Type Safety
Catches type mismatches during development rather than at runtime, preventing crashes that frustrate users and damage app reviews.
Eliminated Type Casting
Generic collections and containers provide type information automatically, removing error-prone casts from your codebase.
Code Reusability
Write once, use everywhere. A generic repository handles users, products, and any other entity with the same implementation.
Flexible APIs
Create flexible, type-safe interfaces for repositories, adapters, and utility functions that work across your entire app.
Generic Classes and Interfaces
Generic classes and interfaces form the foundation of reusable type-safe code in Kotlin. When you define a class or interface with a type parameter, you're creating a template that can work with any concrete type while maintaining type guarantees throughout.
Creating Generic Classes
A generic class declares one or more type parameters in angle brackets after the class name. These parameters serve as placeholders that are replaced with actual types when someone creates an instance. The type parameter can be used throughout the class--in property types, method signatures, and return types--creating a consistent type contract.
For example, a simple Box class can hold any type while maintaining type safety:
class Box<T>(var value: T) {
fun getValue(): T = value
fun setValue(newValue: T) { value = newValue }
}
With this generic class, you can create boxes for any type:
val intBox = Box(42) // Compiler infers Box<Int>
val stringBox = Box("Hello") // Compiler infers Box<String>
val userBox = Box(User()) // Works with your custom types
println(intBox.value) // Already typed as Int, no cast needed
println(stringBox.value) // Already typed as String
The type parameter T acts as a placeholder that becomes concrete when you instantiate the class. The compiler tracks these substitutions, ensuring type safety everywhere the parameter is used.
In mobile development, generic classes appear in countless scenarios. Consider a cache implementation that stores data locally:
class Cache<T>(private val maxSize: Int) {
private val storage = mutableMapOf<String, T>()
fun put(key: String, value: T) {
if (storage.size >= maxSize) {
storage.remove(storage.keys.first())
}
storage[key] = value
}
fun get(key: String): T? = storage[key]
fun contains(key: String): Boolean = storage.containsKey(key)
}
val userCache = Cache<User>(100)
val productCache = Cache<Product>(200)
val stringCache = Cache<String>(50)
Each cache instance maintains strict type safety--users can't accidentally be retrieved as products, and the compiler enforces this correctness.
Generic Interfaces in Mobile Development
Generic interfaces define contracts for reusable data access and transformation layers throughout your mobile application. They're essential for creating abstractions that can work with multiple types while maintaining type safety.
interface Repository<T> {
fun getById(id: String): T?
fun getAll(): List<T>
fun save(entity: T)
fun delete(id: String)
}
interface Mapper<Input, Output> {
fun map(input: Input): Output
}
These interfaces enable powerful abstractions. A repository interface defines CRUD operations that work for any entity type. A mapper interface transforms between data layers with compile-time guarantees. Your implementation code remains the same regardless of the specific types involved.
class UserRepository : Repository<User> {
private val users = mutableMapOf<String, User>()
override fun getById(id: String): User? = users[id]
override fun getAll(): List<User> = users.values.toList()
override fun save(entity: User) { users[entity.id] = entity }
override fun delete(id: String) { users.remove(id) }
}
class ProductRepository : Repository<Product> {
private val products = mutableMapOf<String, Product>()
override fun getById(id: String): Product? = products[id]
override fun getAll(): List<Product> = products.values.toList()
override fun save(entity: Product) { products[entity.id] = entity }
override fun delete(id: String) { products.remove(id) }
}
For cross-platform mobile development, generic interfaces are particularly valuable because they define contracts that can be implemented identically across platforms. Your data layer can use the same Repository<T> interface in shared code, with platform-specific implementations handling the actual persistence.
Type Parameter Naming Conventions
Kotlin conventions help make generic code more readable across your development team. While you can name type parameters anything, consistent naming improves clarity and maintainability.
Standard conventions include T for "Type" in simple cases, E for "Element" (commonly used in collections), and K and V for "Key" and "Value" (standard for maps). For more complex scenarios or multiple type parameters, descriptive names improve understanding.
// Standard conventions
class Box<T>
class List<E>
class Map<K, V>
class Repository<T>
// When clarity demands more
class CacheEntry<Key : Any, Value : Any>
class ApiResponse<RequestId, Payload>
// Multiple related parameters
class Result<Success, Error>
class Pair<First, Second>
Consistency matters more than the specific choice--pick clear names and use them consistently throughout your codebase. This consistency makes your generic code more approachable for team members and reduces cognitive load when reading complex generic signatures.
For more advanced Kotlin techniques, explore our guide on Kotlin coroutines and how they integrate with generic patterns for asynchronous mobile development.
1class Box<T>(var value: T) {2 fun getValue(): T = value3 fun setValue(newValue: T) { value = newValue }4}5 6// Usage7val intBox = Box(42)8val stringBox = Box("Hello")9val userBox = Box(User())10 11// Generic interface12interface Repository<T> {13 fun getById(id: String): T?14 fun getAll(): List<T>15 fun save(entity: T)16 fun delete(id: String)17}18 19class UserRepository : Repository<User> {20 private val users = mutableMapOf<String, User>()21 22 override fun getById(id: String): User? = users[id]23 override fun getAll(): List<User> = users.values.toList()24 override fun save(entity: User) { users[entity.id] = entity }25 override fun delete(id: String) { users.remove(id) }26}27 28class ProductRepository : Repository<Product> {29 private val products = mutableMapOf<String, Product>()30 31 override fun getById(id: String): Product? = products[id]32 override fun getAll(): List<Product> = products.values.toList()33 override fun save(entity: Product) { products[entity.id] = entity }34 override fun delete(id: String) { products.remove(id) }35}Generic Methods and Functions
Generic functions define type parameters at the call site, enabling utility functions that work with any type while callers specify exactly which type to use. This flexibility allows you to create reusable utilities that maintain type safety across your entire codebase.
Declaring Generic Functions
Generic functions declare their type parameters before the function name, inside angle brackets. These parameters can appear anywhere in the function signature--in parameter types, return types, and local variable types:
fun <T> singletonList(item: T): List<T> {
return listOf(item)
}
fun <T> printItem(item: T) {
println("Item: $item")
}
fun <T> mutableListOf(vararg items: T): MutableList<T> {
return items.toMutableList()
}
When calling generic functions, the type parameter is often inferred automatically, but you can specify it explicitly if needed:
val numbers = singletonList(42) // Inferred as List<Int>
val strings = singletonList("hello") // Inferred as List<String>
val explicit: List<User> = singletonList<User>(User()) // Explicit type parameter
printItem(3.14) // Inferred as Double
printItem("message") // Inferred as String
Type inference means your generic code reads naturally without verbose type specifications. The compiler examines your arguments and return context to determine the appropriate type parameters automatically.
Extension Functions with Generics
Kotlin's extension functions work beautifully with generics, enabling you to add functionality to any type without modifying its source code. This pattern is especially powerful in mobile development for adding convenience methods to common types:
fun <T> T.toList(): List<T> = listOf(this)
fun <T, R> T.map(transform: (T) -> R): R = transform(this)
fun <T : Any> T?.orThrow(exception: Throwable): T {
return this ?: throw exception
}
// Usage
val item = "hello".toList() // ["hello"]
val length = 42.map { "Number: $it" } // "Number: 42"
val user = null.orThrow(IllegalStateException("User required"))
Generic Constraints
Sometimes you need to restrict the types that can be used as type arguments. Generic constraints allow you to specify bounds that type parameters must satisfy, enabling you to call methods on constrained types with compile-time guarantees.
The most common constraint is an upper bound, specified with a colon. The type parameter must be a subtype of the bound:
fun <T : Comparable<T>> findMaximum(items: List<T>): T? {
if (items.isEmpty()) return null
return items.maxByOrNull { it }
}
val numbers = listOf(3, 1, 4, 1, 5, 9, 2)
val maximum = findMaximum(numbers) // 9, inferred as Int
For mobile developers, upper bounds are essential when working with Comparable types, Number subclasses, and Android-specific base classes:
fun <T : android.os.Parcelable> saveToBundle(parcelable: T): android.os.Bundle {
return android.os.Bundle().apply {
putParcelable("key", parcelable)
}
}
When you need multiple constraints, use a where clause. Multiple constraints require the type argument to satisfy all specified bounds simultaneously:
fun <T> processItem(
item: T,
processor: (T) -> String
): List<String> where T : Comparable<T>, T : java.io.Serializable {
return listOf(processor(item), item.toString())
}
Understanding these constraint patterns is essential when working with Kotlin extensions in Android development.
1fun <T> singletonList(item: T): List<T> {2 return listOf(item)3}4 5fun <T> printItem(item: T) {6 println("Item: $item")7}8 9// Type inference means explicit type parameters are often unnecessary10val numbers = singletonList(42) // Inferred as List<Int>11val strings = singletonList("hello") // Inferred as List<String>12 13// Upper bound constraint14fun <T : Comparable<T>> findMaximum(items: List<T>): T? {15 return items.maxByOrNull { it }16}17 18val maximum = findMaximum(listOf(3, 1, 4, 1, 5, 9)) // 919 20// Multiple constraints with where clause21fun <T> processItem(22 item: T,23 processor: (T) -> String24): List<String> where T : Comparable<T>, T : java.io.Serializable {25 return listOf(processor(item), item.toString())26}27 28// Android Parcelable constraint29fun <T : android.os.Parcelable> saveToBundle(parcelable: T): android.os.Bundle {30 return android.os.Bundle().apply {31 putParcelable("key", parcelable)32 }33}Understanding Variance: The 'in' and 'out' Modifiers
Variance determines how type relationships between generic types propagate--whether a Box<String> can be treated as a Box<Any>, for example. This concept is crucial for writing correct, flexible APIs that play well with Kotlin's type system.
The Problem with Invariance
By default, generic types in Kotlin are invariant: List<String> is NOT a subtype of List<Any>, even though String is a subtype of Any. This design prevents potential runtime errors. If you could treat a List<String> as a List<Any>, code could add Integers to what should be a list of Strings, causing ClassCastExceptions when reading:
// This is why invariance matters:
val strings: List<String> = listOf("hello", "world")
val anyList: List<Any> = strings // Not allowed in Kotlin!
anyList.add(42) // This would corrupt the string list
val first: String = strings[0] // ClassCastException would occur
Java解决这个问题使用通配符(wildcards),而Kotlin提供了更优雅的解决方案:声明点变型(declaration-site variance)和类型投影(type projections).
The 'out' Modifier for Covariance
When a generic type parameter only produces values (never consumes them), you can declare it as covariant using the out keyword. This makes the type covariant: if T is a subtype of U, then Producer<T> is a subtype of Producer<U>:
interface Producer<out T> {
fun produce(): T
fun getDefault(): T? = null
}
fun demo(strings: Producer<String>) {
val anyProducer: Producer<Any> = strings // Allowed because T is out
val result: Any = strings.produce() // Safe: produce returns T (subtype of Any)
}
The out modifier means T can only appear in output positions--return types and val properties. It cannot appear in input positions--function parameters. This restriction ensures type safety while enabling subtyping relationships.
In mobile development, out appears in producer-like interfaces:
interface DataSource<out T> {
fun fetch(): T
fun getCached(): T?
}
interface ViewModel<out State> {
fun getState(): State
}
// This enables:
val userDataSource: DataSource<User> = UserRepository()
val anyDataSource: DataSource<Any> = userDataSource // Covariance allows this
The 'in' Modifier for Contravariance
When a generic type parameter only consumes values (never produces them), you can declare it as contravariant using the in keyword. This makes the type contravariant: if T is a subtype of U, then Consumer<T> is a supertype of Consumer<U>:
interface Consumer<in T> {
fun consume(item: T)
}
fun demo(numbers: Consumer<Number>) {
val anyConsumer: Consumer<Any> = numbers // Contravariance: Number is subtype of Any
anyConsumer.consume(42) // Numbers can consume Any values
}
The in modifier means T can only appear in input positions--function parameters. It cannot appear in output positions:
interface Comparator<in T> {
fun compare(a: T, b: T): Int
}
val anyComparator: Comparator<Any> = Comparator<String> { a, b ->
a.compareTo(b) // Strings are comparable, so this is valid
}
Consumer-Producer Mnemonic
The classic PECS mnemonic (Producer-Extends, Consumer-Super) from Java applies here, but Kotlin's in/out keywords are more intuitive. Remember: "Consumer in, Producer out!" -- use 'in' for consumers and 'out' for producers.
This mnemonic helps you quickly determine which variance modifier to use when designing generic interfaces. If your interface only returns values of type T, use 'out'. If it only accepts values of type T, use 'in'. This simple rule prevents most variance-related bugs in your mobile applications.
These variance patterns are especially important when working with Android intent filters and data binding in Android development.
1// Covariant producer - 'out' T can only be returned2interface Producer<out T> {3 fun produce(): T4 fun getDefault(): T? = null5}6 7// This works because Producer<String> produces Strings (subtypes of Any)8fun demo(strings: Producer<String>) {9 val anyProducer: Producer<Any> = strings // Allowed10 val result: Any = strings.produce() // Safe11}12 13// Contravariant consumer - 'in' T can only be consumed14interface Consumer<in T> {15 fun consume(item: T)16}17 18// Consumer<Number> can be used where Consumer<Any> is expected19fun demo(numbers: Consumer<Number>) {20 val anyConsumer: Consumer<Any> = numbers21 anyConsumer.consume(42) // Numbers can consume Any values22}23 24// Practical Android example25interface DataSource<out T> {26 fun fetch(): T27 fun getCached(): T?28}29 30class UserDataSource : DataSource<User> {31 override fun fetch(): User = api.getUser()32 override fun getCached(): User? = cache.getUser()33}34 35// Enables flexible subtyping36val userDS: DataSource<User> = UserDataSource()37val anyDS: DataSource<Any> = userDS // Covariance in actionType Projections and Star-Projections
Sometimes you can't declare variance at the type level but need flexible type usage. Type projections allow you to specify variance at the use site, restricting what operations are permitted on the projected type.
Use-Site Variance
When a class can't be declared with out or in at the definition, you can project it at the use site:
class Container<T>(var item: T)
fun copy(from: Container<out Any>, to: Container<Any>) {
to.item = from.item // Only get() is allowed on 'from'
}
val stringContainer = Container("hello")
val anyContainer = Container<Any>("world")
copy(stringContainer, anyContainer) // OK: String is subtype of Any
The out Any projection means you can only read from the container, not write to it. This is Kotlin's equivalent of Java's ? extends Any wildcard but more explicit.
Similarly, in projections restrict reading:
fun fill(container: Container<in String>, value: String) {
container.item = value // Only set() is allowed
}
val anyContainer = Container<Any>(0)
fill(anyContainer, "hello") // OK: Any container accepts String
Star-Projections
When you know nothing about a type argument but need type-safe operations, star-projections provide a safe default:
fun printItems(list: List<*>) {
for (item in list) {
println(item)
}
}
val unknown: List<Any?> = listOf("string", 42, null)
printItems(unknown) // Works with any list
Star-projections have specific meanings based on variance:
- For Producer<out T>, Producer<*> is Producer<out UpperBound>
- For Consumer<in T>, Consumer<*> is Consumer<in Nothing>
- For Container<T>, you can read as Container<out UpperBound> and write to Container<in Nothing>
Star-projections are safe but lose specificity--they're Kotlin's safe equivalent to Java's raw types. Use them when you need to work with generic types but don't care about the specific type parameter.
When working with complex data transformations in mobile apps, understanding these projections helps you build more robust data binding patterns for Android applications.
Type Erasure and Reified Type Parameters
Understanding type erasure is essential for writing correct generic code and debugging type-related issues. Kotlin, like Java, erases generic type information at runtime, but provides mechanisms to work around this limitation when needed.
How Type Erasure Works
During compilation, the Kotlin compiler performs all its type safety checks, ensuring your generics are used correctly. At runtime, however, generic type parameters are removed--the JVM only sees the raw types:
class Box<T>(val value: T)
val stringBox = Box("hello") // At runtime: Box
val intBox = Box(42) // At runtime: Box
// These are the same runtime class:
stringBox::class == intBox::class // true
Type erasure means you cannot check type arguments at runtime in most cases:
fun checkType(items: List<*>) {
// This won't compile:
// if (items is List<Int>) { ... }
// But this works - checking against star-projected type:
if (items is List<*>) {
println("It's a list")
}
}
The erasure design exists for historical JVM reasons and provides some benefits--smaller compiled code, simpler runtime--but requires you to think about when type information is available.
Reified Type Parameters for Inline Functions
Kotlin provides a powerful workaround for type erasure: reified type parameters. When you declare a function as inline and mark a type parameter as reified, the actual type is preserved at runtime and can be used in type checks and casts:
inline fun <reified T> printTypeName(value: Any) {
if (value is T) {
println("The type is: ${T::class.simpleName}")
} else {
println("Type mismatch!")
}
}
printTypeName<String>("hello") // The type is: String
printTypeName<Int>("hello") // Type mismatch!
Reified parameters work because inline functions have their body substituted at the call site, type and all. This makes them invaluable for type-safe generic operations in mobile development.
In mobile development, reified types are commonly used for:
- Type-safe view finding in Android
- Network response parsing with kotlinx.serialization
- Analytics event logging with type tags
inline fun <reified T : android.view.View> android.view.View.findView(id: Int): T {
return findViewById<T>(id)
}
// Usage
val textView: TextView = findViewById<TextView>(R.id.text_view)
This pattern eliminates the need for unchecked casts and provides compile-time type safety for view lookups, a common source of bugs in Android applications.
Reified type parameters work hand-in-hand with Kotlin filtering operations to create powerful, type-safe data processing pipelines in your mobile applications.
1// Reified type parameters preserve type info at runtime2inline fun <reified T> printTypeName(value: Any) {3 if (value is T) {4 println("The type is: ${T::class.simpleName}")5 } else {6 println("Type mismatch!")7 }8}9 10printTypeName<String>("hello") // The type is: String11printTypeName<Int>("hello") // Type mismatch!12 13// Type-safe list conversion14inline fun <reified T> List<*>.asListOfType(): List<T>? {15 if (all { it is T }) {16 @Suppress("UNCHECKED_CAST")17 return this as List<T>18 }19 return null20}21 22val mixed: List<Any?> = listOf("a", "b", "c")23val strings = mixed.asListOfType<String>() // ["a", "b", "c"]24val ints = mixed.asListOfType<Int>() // null25 26// Android view finding utility27inline fun <reified T : android.view.View> android.view.View.findView(id: Int): T {28 return findViewById<T>(id)29}30 31// Usage32val textView: TextView = findViewById<TextView>(R.id.text_view)Best Practices for Mobile Development
Applying generics effectively in mobile projects requires understanding common patterns and avoiding pitfalls. These best practices will help you write cleaner, safer code across your Android and cross-platform applications.
When to Use Generics
Generics excel in specific scenarios in mobile development. Use them when building collections that must maintain type safety--your lists, sets, and maps already use generics, and any custom collection should too. Use generics when creating data access layers like repositories and DAOs that handle multiple entity types. Use generics for adapter classes that render different data types in RecyclerViews or Jetpack Compose. Use generics for utility functions that perform type-agnostic operations.
Don't use generics when the type is truly fixed, when it would overcomplicate simple code, or when runtime type information is more appropriate than compile-time guarantees.
Avoiding Common Mistakes
Several pitfalls trip up developers learning generics. Type erasure means you can't use type parameters for is-checks outside inline functions--use reified parameters or pass Class objects when needed. Unchecked casts require @Suppress annotations and should be used sparingly with clear documentation. Excessive nesting with generic types creates code that's hard to read and maintain.
// Pitfall: unchecked cast without documentation
@Suppress("UNCHECKED_CAST")
fun <T> castTo(value: Any): T = value as T
// Better: reified version when possible
inline fun <reified T> safeCast(value: Any): T? {
return value as? T
}
// Avoid: deeply nested generics
// Instead of: Map<String, Pair<List<User>, Map<String, User>>>
data class UserGroup(
val name: String,
val members: List<User>,
val contacts: Map<String, User>
)
Repository Pattern with Generics
The repository pattern uses generics to create uniform data access across entities, reducing boilerplate and ensuring type safety:
interface Repository<T> {
suspend fun getById(id: String): Result<T>
suspend fun getAll(): Result<List<T>>
suspend fun save(entity: T): Result<Unit>
suspend fun delete(id: String): Result<Unit>
}
class UserRepository(
private val api: UserApi,
private val local: UserDao
) : Repository<User> { ... }
class ProductRepository(
private val api: ProductApi,
private val local: ProductDao
) : Repository<Product> { ... }
RecyclerView Adapters with Generics
RecyclerView adapters benefit from generics for type-safe binding, eliminating the need for multiple adapter implementations:
class GenericAdapter<T>(
private val items: List<T>,
private val bindView: (T, ViewHolder) -> Unit,
private val onClick: (T) -> Unit
) : RecyclerView.Adapter<GenericAdapter<T>.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
bindView(item, holder)
holder.itemView.setOnClickListener { onClick(item) }
}
}
// Usage - type-safe adapter for any data type
val adapter = GenericAdapter<User>(users) { user, holder ->
holder.itemView.findViewById<TextView>(R.id.name).text = user.name
}
This pattern eliminates the need to create separate adapters for each data type, reducing code duplication and ensuring type safety throughout your UI layer.
By following these patterns, you'll write more maintainable Kotlin code. For teams looking to accelerate their mobile development, our AI-powered development services can help automate repetitive coding patterns and improve code quality across your projects.
1// Generic repository interface for type-safe data access2interface Repository<T> {3 suspend fun getById(id: String): Result<T>4 suspend fun getAll(): Result<List<T>>5 suspend fun save(entity: T): Result<Unit>6 suspend fun delete(id: String): Result<Unit>7}8 9// Repository implementations per entity type10class UserRepository(11 private val api: UserApi,12 private val local: UserDao13) : Repository<User> { ... }14 15class ProductRepository(16 private val api: ProductApi,17 private val local: ProductDao18) : Repository<Product> { ... }19 20// Generic RecyclerView adapter for type-safe binding21class GenericAdapter<T>(22 private val items: List<T>,23 private val bindView: (T, ViewHolder) -> Unit,24 private val onClick: (T) -> Unit25) : RecyclerView.Adapter<GenericAdapter<T>.ViewHolder>() {26 27 inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)28 29 override fun onBindViewHolder(holder: ViewHolder, position: Int) {30 val item = items[position]31 bindView(item, holder)32 holder.itemView.setOnClickListener { onClick(item) }33 }34 35 override fun getItemCount(): Int = items.size36}37 38// Usage - one adapter type for all data types39val userAdapter = GenericAdapter<User>(users) { user, holder ->40 holder.itemView.findViewById<TextView>(R.id.name).text = user.name41}42 43val productAdapter = GenericAdapter<Product>(products) { product, holder ->44 holder.itemView.findViewById<TextView>(R.id.product_name).text = product.name45}Generics in Kotlin Multiplatform
For cross-platform projects, generics enable significant code sharing while maintaining platform-specific implementations. Your business logic can operate on generic types while platform-specific code handles platform-specific requirements.
// Shared interface in common module
interface Storage<T> {
fun save(item: T)
fun load(): T?
}
// Platform implementations
class IOSStorage<T : NSCoding> : Storage<T> {
override fun save(item: T) {
// iOS-specific implementation using NSCoding
}
override fun load(): T? {
// iOS-specific loading logic
}
}
class AndroidStorage<T : android.os.Parcelable> : Storage<T> {
override fun save(item: T) {
// Android-specific implementation using Parcelable
}
override fun load(): T? {
// Android-specific loading logic
}
}
// Shared usage - same code works across platforms
class UserPreferences<T>(
private val storage: Storage<T>,
private val defaultValue: T
) {
var preference: T by Delegates.vetoable(storage.load() ?: defaultValue) {
_, _, newValue ->
storage.save(newValue)
true
}
}
This approach maximizes shared code while maintaining the native performance and capabilities users expect on each platform. The generic Storage<T> interface defines the contract in shared code, while platform-specific implementations handle the details of each platform's persistence mechanisms.
When building cross-platform mobile applications with Kotlin Multiplatform, generics become essential for sharing business logic while respecting platform differences. Combined with our web development services, you can build full-stack applications that share code between mobile and web platforms.