LiveData를 사용할 때 LiveData가 변경되었을 때 값을 수신하고 다른 형태의 데이터로 변환해야 하는 경우가 있다.
예를들면 아래와 같은 상황을 보자.
data class User(
val firstName: String,
val lastName: String,
val nickName: String,
...
)
private val _userLiveData: MutableLiveData<User> = MutableLiveData()
User 객체를 얻어왔을 때, UI에는 firstName과 lastName만을 수신해야한다고 가정해보자.
Activity/Fragment에서 User를 UI에서 가공해야 할까? 이는 바람직하지 않다.
왜냐하면 데이터의 가공은 ViewModel 레이어에서 이루어지고, UI는 단지 값을 수신받아 UI를 변경하는 역할만을 수행해야 한다.
그렇지 않으면 객체가 변경되거나 ViewModel의 비즈니스 로직이 변경되었을 때 UI도 영향이 생길 가능성이 높아진다.
그럼 다음과 같이 생각할 수 있다.
아래 코드처럼 ViewModel에서 User객체 얻었을 때 다른 MutableLiveData에 값을 변경해주면 되겠네?
private val _userLiveData: MutableLiveData<User> = MutableLiveData()
// 추가
private val _userName: MutableLiveData<String> = MutableLiveData()
val userName: LiveData<String> = _userName
이것은 어떤 문제가 있을까?
데이터가 필요할 때 마다 MutableLiveData 생성하고, UI에서 수신할 LiveData 생성하고의 반복이다.
즉, 로직도 늘어나고 MutableLiveData LiveData도 계속 늘어난다.
뭔가 불편함이 느껴질 것이다.
이 문제를 해소하기 위해, 어떤 LiveData가 변경되었을 때 다른 LiveData로 변경할 수 있는 기능인
Transformations.map, Transformations.switchMap 을 소개한다.
Transformations.map
package androidx.lifecycle;
public class Transformations {
@MainThread
public static <X, Y> LiveData<Y> map(
@NonNull LiveData<X> source,
@NonNull final Function<X, Y> mapFunction) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(source, new Observer<X>() {
@Override
public void onChanged(@Nullable X x) {
result.setValue(mapFunction.apply(x));
}
});
return result;
}
}
Transformations.map 함수의 구현을 보자.
Transformations.map 함수는 어떤 LiveData를 다른 LiveData로 결과물을 반환한다.
source로 다른 LiveData를 전달받고, 이 LiveData의 값을 수신했을 때 값을 어떻게 변경할 것인지에 대한 map 함수를 받는다.
내부에 MediatorLiveData가 사용된 것을 확인할 수 있는데, 이에 대해선 다음 포스팅에서 다룰 예정이다.
그리고 아래와 같이 사용할 수 있다.
private val _userLiveData: MutableLiveData<User> = MutableLiveData()
// 추가
val userName: LiveData<String> = Transformations.map(_userLiveData) {
it.firstName + " " + it.lastName
}
이렇게 되면 userLiveData의 값 변경에만 신경을 쓰고, 하나의 LiveData만 추가 함으로써 문제를 해결할 수 있다.
Transformations.switchMap
package androidx.lifecycle;
public class Transformations {
@MainThread
public static <X, Y> LiveData<Y> switchMap(
@NonNull LiveData<X> source,
@NonNull final Function<X, LiveData<Y>> switchMapFunction) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(source, new Observer<X>() {
LiveData<Y> mSource;
@Override
public void onChanged(@Nullable X x) {
LiveData<Y> newLiveData = switchMapFunction.apply(x);
if (mSource == newLiveData) {
return;
}
if (mSource != null) {
result.removeSource(mSource);
}
mSource = newLiveData;
if (mSource != null) {
result.addSource(mSource, new Observer<Y>() {
@Override
public void onChanged(@Nullable Y y) {
result.setValue(y);
}
});
}
}
});
return result;
}
}
Transformations.map 을 이해했다면 Transformations.switchMap도 쉽게 이해할 수 있다.
동일하게 source로 LiveData를 받고, 함수의 결과로 변경된 LiveData를 반환한다.
그러나 두번째 파라미터인 map 함수의 결과값은 LiveData가 된다.
앞선 문제를 해결하기 위해 Transformations.map을 사용했지만, Transformations.switchMap은 활용처가 다르다.
예를들면 아래와 같다.
private val _userLiveData: MutableLiveData<User> = MutableLiveData()
// 추가
val userName: LiveData<String> = Transformations.switchMap(_userLiveData) {
getUserNameById(it.id)
}
fun getUserNameById(userId: Int): LiveData<String> {
// do someting
}
어떤 값의 변화로 인해 로직을 통해 값을 LiveData로 얻는 등의 경우에 사용된다.
Room DataBase에 접근해서 얻는다던지 등의 경우가 되겠다.
이번 포스팅에선 LiveData의 값이 변화되었을 때 이를 수신하여 값을 가공한 또 다른 LiveData를 다루는 법을 알아냈다.
Transformations.map, Transformations.switchMap의 내부 구현과 차이점을 분명히 안다면, 적절한 곳에 활용할 수 있을 것이다.
두 함수의 내부 구현에서 MediatorLiveData가 사용된 것을 확인할 수 있었다.
이 역시 자주 활용되는 것 중 하나이므로, 다음 포스팅에서 알아보겠다.
'개발 > Android' 카테고리의 다른 글
Kotlin Multiplatform Mobile(KMM) - Start! (0) | 2023.04.05 |
---|---|
Android - Deep dive into LiveData - 4. MediatorLiveData (0) | 2023.04.01 |
Android - Deep dive into LiveData - 2. setValue vs postValue (0) | 2023.01.21 |
Android - Deep dive into LiveData - 1. LiveData가 무엇인가요? (0) | 2023.01.21 |
Android - LifecycleOwner vs ViewLifecycleOwner (0) | 2023.01.15 |