Kotlin/kotlin-style-guide

Using scope functions apply/with/run/also/let

Opened this issue ยท 7 comments

apply

Use apply for initialization:

val foo = createBar().apply {
    property = value
    init()
}

also

Use also over apply if the receiver is used for anything other than setting properties or function calls on it:

class Baz {
    var currentBar: Bar?
    val observable: Observable

    val foo = createBar().also {
        currentBar = it
        observable.registerCallback(it)
    }
}

Prefer also over apply if there already are multiple receivers in scope, especially if you make calls on any outer receivers:

class Foo {
    fun Bar.baz() {
        val stuff = callSomething().also {
            it.init()
            this@baz.registerCallback(it)
        }
    }
}

apply/with/run

Prefer apply/run over with if the receiver is nullable.

getNullable()?.run {
    init()
}

getNullable()?.apply {
    init()
}

Prefer run/with over apply if the returned value is not used

view.run {
    textView.text = "Hello World"
    progressBar.init()
}

with(view) {
    textView.text = "Hello World"
    progressBar.init()
}

Choose one of the above and use it consistently.

let

Prefer let over run in method chains that transform the receiver

val baz: Baz = foo.let { createBar(it) }.convertBarToBaz()
// or with function references
val baz: Baz = foo.let(::createBar).convertBarToBaz()

I use with over run if the object in question is a local variable or a short expression:

with(view) {
    textView.text = "Hello World"
    progressBar.init()
}

On the other hand, I use run in the end of a long call chain:

MyApp.defaultActivity.getActiveView().view.run {
    textView.text = "Hello World"
    progressBar.init()
}

@cypressious Why you prefer run over apply if the returned value is not used? run is also returning value - the result of function literal. The same with with, but for this one, I also feel intuitively that it should be used this way.

I use let when I just need to unpack nullable read-write property (when smart-casting is not working):

var text: String? = null
fun showText() {
    text?.let { text -> 
        println(text) 
    }
}

I often see let used this way.

@MarcinMoskala it's just to differentiate. I use apply when I need to the receiver as the return value, otherwise I don't use it.

Another good use of with is for executing a lambda in the context of a casted object:

with(activity as MyActivity) {
  myActivitymethod()
  ...
}

Using also for a "singleton with parameter" (from Google's architecture components sample code):

class UsersDatabase : RoomDatabase() {

    companion object {

        @Volatile private var INSTANCE: UsersDatabase? = null

        fun getInstance(context: Context): UsersDatabase =
                INSTANCE ?: synchronized(this) {
                    INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
                }

        private fun buildDatabase(context: Context) =
                Room.databaseBuilder(context.applicationContext,
                        UsersDatabase::class.java, "Sample.db")
                        .build()
    }
}

https://github.com/googlesamples/android-architecture-components/blob/master/BasicRxJavaSampleKotlin/app/src/main/java/com/example/android/observability/persistence/UsersDatabase.kt

I was trying out what to use when in the same year when this issue was created. My understanding back then is that these functions should be used based on intention:

  • apply: post-construction configuration
  • with: configure object created somewhere else
  • also: additional processing on an object in a call chain
  • let: conversion of value
  • run: execute lambda with side-effects and no result