๐ŸŸฃ Kotlin Programming

Kotlin Delegation and Property Delegates

Every time you write by viewModels(), by lazy or var x by remember { }, you're using Kotlin delegation. Understanding how delegates work lets you remove boilerplate, prefer composition over inheritance, and write your own reusable property behaviours.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Advanced
On this page
  1. Class delegation: composition without boilerplate
  2. Property delegation: how by works
  3. Built-in delegates you'll use
  4. Android and Compose delegates
  5. Writing your own delegate
  6. Real-world uses
  7. Common mistakes
  8. Frequently asked questions
  9. Conclusion

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:

Decorating a repository with loggingKotlin
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(...).

How a delegated property works Reading the property calls the delegate's getValue function; writing calls setValue. The delegate object stores or computes the value. var token by Pref()your property read โ†’ getValue() write โ†’ setValue() Delegate objectholds the logic Storagememory, prefs, state
The property looks ordinary to callers. The delegate decides where the value lives and what happens on access.

Built-in delegates you'll use

lazy

Expensive objects created on first useKotlin
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

React to changesKotlin
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

Read properties from a mapKotlin
class FeatureFlags(values: Map<String, Any?>) {
    val newCheckout: Boolean by values
    val maxUploadMb: Int by values
}

Android and Compose delegates

ViewModelsKotlin
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
}
Compose stateKotlin
@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

A trimmed, length-limited string propertyKotlin
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)
}
Fragment argument delegate (lightweight)Kotlin
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 lazy for 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.getValue and setValue. Without them, by remember doesn'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 real captures the object at construction, so reassigning a var later 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.

See all our apps โ†’

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.

Share: LinkedIn X / Twitter WhatsApp