Llifecycle, LiveData 라이브러리의 Transformations.map(), Transformations.switchMap()의 차이점에 대해서 소개합니다.
map()과 switchMap()은 LiveData를 위한 라이브러리로, LiveData<X> 타입 데이터를 LiveData<Y> 타입 데이터로 변환한다는 공통점이 있지만, 사용 방법 및 목적에서 차이점이 있습니다.
1. 리턴 타입
표면적으로 가장 단순한 차이점은 func의 리턴 타입이 다르다는 것입니다.
LiveData<Y> map (LiveData<X> source, Function<X, Y> func):LiveData<X>타입 데이터를LiveData<Y>타입 데이터로 변환합니다. func에서는 Y 타입의 객체를 리턴.LiveData<Y> switchMap (LiveData<X> trigger, Function<X, LiveData<Y>> func):LiveData<X>타입 데이터를LiveData<Y>타입 데이터로 변환합니다. func에서는LiveData<Y>타입의 객체를 리턴.
map()
map()은 아래와 같이 사용할 수 있는데, 데이터 변환을 처리하는 함수에서 String 타입의 객체를 리턴하면 됩니다. 그럼 map() 내부에서 LiveData<String> 타입의 객체를 리턴합니다.
val userLiveData : LiveData<User> = MutableLiveData(User("Jone", "Doe"))
val userName : LiveData<String> = Transformations.map(userLiveData) { user ->
user.firstName + user.lastName
}switchMap()
switchMap()은 데이터 변환을 처리하는 함수에서 LiveData<String> 타입의 객체를 리턴해야 합니다.
val userLiveData : LiveData<User> = MutableLiveData(User("Jone", "Doe"))
val userName : LiveData<String> = Transformations.switchMap(userLiveData) { user ->
MutableLiveData(user.firstName + user.lastName)
}2. 정적 변환 vs 동적 변환
map과 switchMap을 Google developers에서는 일대일 정적 변환, 일대일 동적 변환이라고 표현하기도 합니다.
map() : 정적 변환
데이터 변화가 있을 때 즉각적으로 변환만 하면 되는 경우, 아래 코드처럼 map()을 사용할 수 있습니다. LiveData인 Source의 변경이 발생하면 데이터가 변환되어 viewModelResult(LiveData)를 수신하는 Observer에게 이벤트가 전달됩니다.
class MainViewModel {
val viewModelResult = Transformations.map(repository.getDataForUser() { data ->
convertDataToMainUIModel(data)
}
}switchMap() : 동적 변환
즉각적으로 데이터 변환이 어려워 초기화 구간에 사용할 수는 없지만, 가까운 미래에 사용해도 된다면 switchMap()으로 구현할 수 있습니다.
switchMap()은 LiveData 객체를 리턴하기 때문에 repository.getDataForUser(user)에서 미래에 사용할 LiveData를 리턴하고, 준비가 끝났을 때 repository.getDataForUser(user) 내부에서 변환된 데이터를 이전에 리턴한 LiveData에 업데이트할 수 있습니다.
class MainViewModel {
val repositoryResult = Transformations.switchMap(userManager.user) { user ->
repository.getDataForUser(user)
}
}3. 단순한 작업 vs 오래 걸리는 작업
map()과 switchMap()이 Main Thread에서 동작할 때, 작업이 오래 걸리면 UI가 일정 시간 멈추거나 ANR이 발생할 수 있습니다.
Stackoverflow 글을 참고하면, 시간이 오래 안걸리는 작업에서는 map으로 구현하는 것이 적절하고, 시간이 오래 걸리는 작업에서는 switchMap으로 구현하는 것이 좋다고 합니다.
switchMap() : Time consuming tasks
다음은 Stackoverflow의 답변에서 소개된 Java 코드인데, switchMap()은 먼저 LiveData 객체를 리턴하고, 다른 쓰레드에서 오래 걸리는 작업을 수행하고 완료되면 LiveData로 업데이트하도록 구현하였습니다. switchMap()을 호출하는 쓰레드는 바로 LiveData 객체를 받기 때문에 쓰레드가 Block되는 일은 없습니다. 다만, 다른 쓰레드에서 처리가 완료되어야 LiveData에 데이터가 업데이트됩니다.
LiveData<Integer> mCode = Transformations.switchMap(mString, input -> {
final MutableLiveData<Integer> result = new MutableLiveData<>();
new Thread(new Runnable() {
@Override
public void run() {
// Pretend we are busy
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int code = 0;
for (int i=0; i<input.length(); i++) {
code = code + (int)input.charAt(i);
}
result.postValue(code);
}
}).start();
return result;
});4. Transformations 라이브러리 코드
Transformations.java의 map()과 switchMap()의 코드입니다.
map()
LiveData source가 추가된 MediatorLiveData 객체가 리턴됩니다. Source의 업데이트가 발생하면 mapFunction 안에서 변환된 데이터를 MediatorLiveData에 업데이트합니다. 이 과정에서 LiveData<Source> 또는 MediatorLiveData의 객체가 바뀌지 않습니다.
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;
}switchMap()
MediatorLiveData가 리턴되지만, Source의 변경이 있을 때, func에서 리턴되는 LiveData를 새로운 Source로 MediatorLiveData에 설정하고, MediatorLiveData를 업데이트합니다. 이 과정에서 MediatorLiveData는 변하지 않지만, MediatorLiveData가 수신하는 Source가 변경됩니다.
@MainThread
public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger,
@NonNull final Function<X, LiveData<Y>> func) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(trigger, new Observer<X>() {
LiveData<Y> mSource;
@Override
public void onChanged(@Nullable X x) {
LiveData<Y> newLiveData = func.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;
}References
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력