Android - Transformations.map(), switchMap() 차이점

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.javamap()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

Loading script...

Related Posts

codechachaCopyright ©2019 codechacha