이펙티브 코틀린 아이템 21: 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라
이펙티브 코틀린(재사용성)
아이템 21: 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라
property delegate 를 사용하는것이 property delegation 이라고 부른다.
코틀린에서 lazy 함수 제공
val value by lazy { createValue() }
프로퍼티 위임을 사용하면 변화가 있을때 이를 감지하는 observable 패턴을 쉽게 만들수 있다.
var items: List<Item> by Delegates.observable(listOf()){
notifyDataSetChange
}
var key : String? by Delegates.observable(null){_,old,new->
Log.e("key changed from $old to $new")
}
일반적으로 자바를 활용하면 어노테이션을 많이 사용해야 되지만 코틀린은 프로퍼티 위임을 사용해서 간단하고 type-safe 하게 구현 가능
// 안드로이드에서의 뷰와 리소스 바인딩
private val button: Button by bindView(R.id.button)
private val textSize by bindDimension(R.dimen.font_size)
private val doctor: Doctor by argExtra(DOCTOR_ARG)
// kotlin 에서의 종속성 주입
private val presenter: MainPresenter by inject()
private val repository: NetworkRepository by inject()
private val vm: MainViewModel by viewModel()
// 데이터 바인딩
private val port by bindConfiguration("port")
private val token: String by preferences.bind(TOKEN_KEY)
어떻게 위와 같은 코드가 가능하고 프로퍼티 위임을 어떻게 사용할수 있는지 알아보자
var token: String? = null
get() {
print("token returned value $field")
return field
}
set(value) {
print("token changed from $field to $value")
field = value
}
var attempts: Int = 0
get() {
print("attempts returned value $field")
return field
}
set(value) {
print("attempts changed from $field to $value")
field = value
}
두개 프로퍼티 타입은 다르지만 내부적으로 거의 같은 처리를 한다. 그리고 프로젝트에서 자주 쓰일것 같다.
var token: String? by LoggingProperty(null)
var attempts: Int by LoggingProperty(0)
private class LoggingProperty<T>(var value: T) {
operator fun getValue(
thisRef: Any?,
prop: KProperty<*>
): T {
print("${prop.name} returned value $value")
return value
}
operator fun setValue(
thisRef: Any?,
prop: KPRoperty<*>,
newValue: T
) {
val name = prop.name
print("name changed from $value to $newValue")
value = newValue
}
}
프로퍼티 위임이 어떻게 동작하는지 보려면 by 가 어떻게 컴파일 되는지 봐야 된다.
@JvmField
private val 'token$delegate' =
LoggingProperty<String>(null)
var token: String?
get() = 'token$delegate'.getValue(this, ::token)
set(value) {
'token$delegate'.setValue(this, ::token, value)
}
코드를 보면 알수 있듯이 단순하게 값만 처리 하는게 바뀐게 아니라 컨텍스트와 프로퍼티 레퍼런스의 경계도 함께 사용하는 형태로 바뀐다.
여러 종류의 뷰와 함께 사용될 수 있는 델리게이트가 필요한 경우가 있다. 이는 다음과 같이 구현해서, 컨텍스트의 종류에 따라서 적절한 메서드가 선택되게 만들 수 있다.
class SwipeRefreshBinderDelegate(val id: Int) {
private var cache: SwipeRefreshLayout? = null
operator fun getValue(
activity: Activity,
prop: KProperty<*>,
): SwipeRefreshLayout {
return cache?: activity
.findViewById<SwipeRefreshLayout>(id)
.also { cache = it }
}
operator fun getValue(
fragment: Fragment,
prop: KProperty<*>
): SwipeRefreshLayout {
return cache?: fragment.view
.findViewById<SwipeRefreshLayout>(id)
.also { cache = it }
}
}
객체를 프로퍼티 위임하려면 val의 경우 getValue연산, var의 경우 getValue와 setValue 연산이 필요하다.
이러한 연산은 확장 함수로도 만들 수 있다.
val map: Map<String, Any> = mapOf(
"name" to "Marcin",
"kotlinProgrammer" to true
)
val name by map
print(name) // Marcin
inline operator fun <V, V1 : V> Map<in String, V>.getValue(thisRef: Any?, property: KProperty<*>): V1 = getOrImplicitDefault(property.name) as V1
코틀린의 델리게이터들
- lazy
- Delegates.observable
- Delegates.vetoable
- Delegates.notNull