Android Architecture Component 중에 ViewModel 관련 예제를 보던중 특이한 코드가 있었다.

class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val model: MyViewModel by viewModels()
        model.getUsers().observe(this, Observer<List<User>>{ users ->
            // update UI
        })
    }
}

여기서 ViewModel을 생성하는 부분의 코드가 전혀 이해되지 않았다.

val model: MyViewModel by viewModels()

Delegated Properties

우리가 공통적으로 사용하는 lazy properties, observable properties, 그리고 storing properties를 쉽게 구현하기 위해서, Kotlin에서는 Delegated Properties를 지원한다.

레퍼런스 문서에 있는 간단한 예를 살펴 보면, p라는 Delegated PropertiesDelegate클래스를 사용해 선언했다.

class Example {
    var p: String by Delegate()
}
import kotlin.reflect.KProperty

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }
 
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

p를 읽을때는 Delegate 클래스의 getValue()가 호출되고, 첫번째 파라미터는 p를 읽은 객체이고 두번째 파라미터는 p에 대한 KProperty를 전달 받는다.

val e = Example()
println(e.p)

이를 실행하면, 아래와 같이 출력된다.

Example@33a17727, thank you for delegating ‘p’ to me!

마찬가지로 setValue()의 경우에도 마지막 파라미터로 value가 전달되는 것을 제외하면 동일하다

e.p = "NEW"

NEW has been assigned to ‘p’ in Example@33a17727.

Lazy

lazy()는 lambda를 받아서 Lazy<T>를 리턴하는 함수로, lazy property를 구현할 수 있다. 첫번째 get()에서는 전달된 lambda가 실행되고 그 리턴값을 저장한다. 다음 호출부터는 저장된 결과가 리턴된다.

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}

computed!
Hello
Hello

참고로, Lazy.kt를 보면 getValue가 extension으로 정의되있고, T의 인스턴스인 value를 리턴하게 되있다.

@kotlin.internal.InlineOnly
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

viewModels()

따라서, val model: MyViewModel by viewModel()의 의미는 model의 getValue/setValue 동작을 viewModels의 getValue/setValue의 동작에 위임하는 것이다.

아래 코드는 ActivityViewModelLazy.kt이다.

@MainThread
inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
    factory: ViewModelProvider.Factory? = null
): Lazy<VM> = ActivityViewModelLazy(this, VM::class, factory)

/**
 * An implementation of [Lazy] used by [ComponentActivity.viewModels] tied to the given [activity],
 * [viewModelClass], [factory]
 */
class ActivityViewModelLazy<VM : ViewModel>(
    private val activity: ComponentActivity,
    private val viewModelClass: KClass<VM>,
    private val factory: ViewModelProvider.Factory?
) : Lazy<VM> {
    private var cached: VM? = null
    override val value: VM
        get() {
            var viewModel = cached
            if (viewModel == null) {
                val application = activity.application
                        ?: throw IllegalArgumentException("ViewModel can be accessed " +
                                "only when Activity is attached")
                val resolvedFactory = factory ?: AndroidViewModelFactory.getInstance(application)
                viewModel = ViewModelProvider(activity, resolvedFactory).get(viewModelClass.java)
                cached = viewModel
            }
            return viewModel
        }
    override fun isInitialized() = cached != null
}

소스를 보면 model을 접근하면 ActivityViewModelLazyget이 호출되고, ViewModelProvider에서 해당 MyViewModel의 ViewModel을 가져오게 된다.

즉, Java 예제와 비교하면 lazy인 것을 제외하면 비슷한 것이다.

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}