MVVM 패턴에서 LiveData는 반응형 프로그래밍으로써 매우 인기 있고 간단한 접근 방식입니다. Google I/O 2017에서 Kotlin을 공식 언어로 발표함과 동시에 AAC를 발표했지만 LiveData는 Java로 짜인 라이브러리입니다. 이 때문에 Kotlin에서 사용할 때는 몇 가지 문제점이 있습니다.
문제점
Kotlin에서는 null에 대한 체크가 엄격합니다. String과 String?의 차이는 Kotlin을 공부하신 분들이라면 다들 아실 거라고 생각합니다. 아래의 코드에서 이와 관련된 LiveData의 문제점이 있습니다.
val sampleLiveData: LiveData<String> = ...
// nullable
val currentValue = sampleLiveData.value
sampleLiveData.observe(this, Observer {
// nullable
it?.let { sample ->
...
}
}
위의 코드에서 이상한 점을 눈치채셨나요? sampleLiveData의 데이터 타입은 String?이 아닌 String입니다. 따라서 getValue() 함수를 통해 값을 가져왔을 때 String 타입으로 반환될 거라 생각하기 쉽지만, 실제로는 String? 타입으로 반환됩니다. 마찬가지로 Observer의 onChanged 함수에서도 String이 아닌 String? 타입으로 받게 됩니다. 이 때문에 반환된 value 값을 가지고 사용할 때 불필요한 null-check를 해야만 합니다.
이와 같은 일이 생기는 이유가 무엇일까요? 이는 LiveData의 코드를 보면 알 수 있습니다.
/**
* Returns the current value.
* Note that calling this method on a background thread does not guarantee that the latest
* value set will be received.
*
* @return the current value
*/
@SuppressWarnings("unchecked")
@Nullable
public T getValue() {
Object data = mData;
if (data != NOT_SET) {
return (T) data;
}
return null;
}
LiveData의 getValue() 함수를 보면 아직 value가 설정되지 않으면 null 값을 반환하고 있습니다. 따라서 타입을 String으로 지정해줘도 초기값이 설정되지 않으면 null을 반환받을 수 있습니다.
위의 문제는 value를 가져올 때마다 null-check를 하면 된다는 귀찮은 방법이 있습니다. 하지만 이보다 더 큰 문제는 MutableLiveData에 있습니다.
val sampleLiveData: MutableLiveData<String> = ...
sampleLiveData.value = null
문제는 위의 코드가 오류 없이 동작된다는 것에 있습니다. sampleLiveData의 타입을 String으로 지정해줬음에도 불구하고 setValue로 null을 설정해줄 수 있습니다. 이 또한 MutableLiveData가 Java 소스로 구성되어 있어서 생기는 문제점입니다.
해결 방법
LiveData를 사용할 때 불필요한 null-check를 하고 싶지 않고, 실수로 null이 들어가더라도 컴파일러가 알아서 잡아줘서 오류가 생기지 않게 하고 싶습니다. 해결 방법으로 LiveData를 상속받아 NonNullLiveData를 만드는 방법이 있습니다. (이름이 너무 길어 맘에 들진 않습니다.)
open class NonNullLiveData<T: Any>(value: T): LiveData<T>(value) {
override fun getValue(): T {
return super.getValue() as T
}
inline fun observe(owner: LifecycleOwner, crossinline observer: (t: T) -> Unit) {
this.observe(owner, Observer {
it?.let(observer)
})
}
}
class NonNullMutableLiveData<T: Any>(value: T): NonNullLiveData<T>(value) {
public override fun setValue(value: T) {
super.setValue(value)
}
public override fun postValue(value: T) {
super.postValue(value)
}
}
NonNullLiveData의 타입을 지정해줄 때 upper bound를 Any로 지정해줍니다. 이렇게 해주면 nullable인 타입은 받을 수가 없게 됩니다. 그 뒤, getValue()와 setValue()를 재정의해서 NonNullLiveData를 만들 수 있습니다.
한 가지 주의할 점은, 기존 LiveData의 생성자는 LiveData()와 LiveData(T value) 두 가지였습니다. 하지만 빈 생성자로 LiveData를 만들게 되면 값이 설정되지 않을 때 null이 반환돼서 오류가 생길 수 있습니다. 따라서 생성자는 값을 기본적으로 받게끔 설정해줍니다.
이렇게 해서 이제 NonNull 타입일 때는 귀찮게 null-check를 하지 않아도 되고, null을 넣으려고 할 때 컴파일러가 막아줄 수 있게 되었습니다.
참고
proandroiddev.com/improving-livedata-nullability-in-kotlin-45751a2bafb7
'Android' 카테고리의 다른 글
[Android] LeakCanary로 메모리릭 잡기 (0) | 2021.04.17 |
---|---|
[Android] 문자열 단/복수 처리 - plurals (0) | 2021.03.28 |
[Android] RecyclerView에 divider 넣기 - ItemDecoration (1) | 2021.01.16 |
[Android] LiveData setValue() vs postValue() (0) | 2021.01.15 |
[Android] ConstraintLayout 가상 오브젝트 - Guideline, Barrier, Group (0) | 2020.12.27 |