On this page
Kotlin's by keyword shows up everywhere in Android code: private val vm: MainViewModel by viewModels(), val db by lazy { โฆ }, var count by remember { mutableIntStateOf(0) }. They look like magic, but they're one simple language feature applied in two ways: class delegation and property delegation.
Class delegation: composition without boilerplate
"Prefer composition over inheritance" usually means writing many forwarding methods. Kotlin generates them for you:
interface ArticleRepository {
suspend fun getArticles(): List<Article>
suspend fun getArticle(id: String): Article
suspend fun bookmark(id: String)
}
// Delegates every method to `real`, overriding only what it needs.
class LoggingArticleRepository(
private val real: ArticleRepository,
) : ArticleRepository by real {
override suspend fun bookmark(id: String) {
Log.d("Repo", "bookmark($id)")
real.bookmark(id)
}
}This is the Decorator pattern in a few lines: caching, logging and metrics layers can wrap any implementation without inheritance.
Property delegation: how by works
For val x by D, the compiler routes reads of x to D.getValue(thisRef, property), and for var, writes to D.setValue(...).
Built-in delegates you'll use
lazy
class ReportExporter(private val context: Context) {
private val pdfRenderer by lazy { PdfDocumentRenderer(context) } // created only if export is used
}lazy is thread-safe by default (SYNCHRONIZED). Use lazy(LazyThreadSafetyMode.NONE) for values only touched from the main thread, to avoid locking overhead.
observable and vetoable
var volume: Int by Delegates.vetoable(50) { _, _, new -> new in 0..100 } // reject invalid values
var theme: Theme by Delegates.observable(Theme.SYSTEM) { _, old, new ->
if (old != new) analytics.log("theme_changed", new.name)
}Map-backed properties
class FeatureFlags(values: Map<String, Any?>) {
val newCheckout: Boolean by values
val maxUploadMb: Int by values
}Android and Compose delegates
class ProfileActivity : ComponentActivity() {
private val viewModel: ProfileViewModel by viewModels() // scoped to this Activity
}
class OrdersFragment : Fragment() {
private val shared: CartViewModel by activityViewModels() // shared with the host Activity
}@Composable
fun SearchBox() {
// MutableState has getValue/setValue operators, so `by` gives a plain-looking variable.
var query by rememberSaveable { mutableStateOf("") }
TextField(value = query, onValueChange = { query = it })
}Without by, you'd write query.value everywhere. The delegate also makes Compose track reads correctly, so reading query subscribes the composable to changes.
Writing your own delegate
class Trimmed(private val maxLength: Int) : ReadWriteProperty<Any?, String> {
private var value = ""
override fun getValue(thisRef: Any?, property: KProperty<*>): String = value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = value.trim().take(maxLength)
}
}
class ProfileForm {
var displayName: String by Trimmed(maxLength = 40)
var bio: String by Trimmed(maxLength = 160)
}fun <T> Fragment.argument(key: String): ReadOnlyProperty<Fragment, T> =
ReadOnlyProperty { thisRef, _ ->
@Suppress("UNCHECKED_CAST")
requireNotNull(thisRef.arguments?.get(key) as T?) { "Missing argument '$key'" }
}
// class DetailFragment : Fragment() { private val itemId: String by argument("itemId") }Real-world uses
Caching decorator
Class delegation wraps a network repository with an in-memory cache, overriding only the read methods.
Lazy SDK clients
by lazy defers creating expensive clients until first use, helping startup time.
Validated settings
A custom delegate enforces ranges or trimming wherever a property is assigned.
Common mistakes
- Using
by lazyfor values that depend on a Context or View that may be replaced, such as Fragment views, which leaks old instances. - Forgetting the imports
androidx.compose.runtime.getValueandsetValue. Without them,by rememberdoesn't compile. - Hiding heavy work (I/O) inside
getValue, where it's invisible to readers and may run on the main thread. - Class delegation to a mutable reference:
by realcaptures the object at construction, so reassigning avarlater doesn't change the delegate.
Frequently asked questions
Is by lazy the same as lateinit?
No. lazy is for vals computed on first access. lateinit is for vars assigned later from outside. See Kotlin Null Safety.
Does delegation have a performance cost?
A small one: an extra object and an indirect call. It's negligible for typical use. The compiler can also optimise some cases.
Can interfaces with default methods be delegated?
Yes. Delegation forwards all interface members, including those with default implementations, to the delegate.
Conclusion
Class delegation gives you composition and decorators without boilerplate. Property delegation moves storage and access logic into reusable objects, from lazy and viewModels() to Compose's by remember. Once you see the getValue/setValue mechanism, these idioms stop being magic.
Next step: decide how far Kotlin can take your codebase with Kotlin Multiplatform vs Native Android.
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.