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.静的変換対動的変換

mapとswitchMapをGoogle developersでは、1対1の静的変換、1対1の動的変換と表現することもあります。

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 の Observerにイベントを送信できます。

class MainViewModel {
  val repositoryResult = Transformations.switchMap(userManager.user) { user ->
     repository.getDataForUser(user)
  }
}

3. 単純なタスク vs 時間がかかるタスク

map()switchMap() が Main Thread で動作するとき、操作が長くかかると UI が一定時間停止するか ANR が発生することがあります。

Stackoverflowが適切で、時間がかかる作業では switchMap で実装するのが良いという。

switchMap() : Time consuming tasks

以下はStackoverflowの回答で紹介されたJavaコードです。 switchMap()はまずLiveDataオブジェクトを返し、他のスレッドで長くかかる作業を行い、完了するとLiveDataに更新するように実装しました。

switchMap() を呼び出すスレッドは LiveData オブジェクトを直接受け取るので、スレッドがブロックされることはありません。ただし、他のスレッドで処理が完了しなければ、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

codechachaCopyright ©2019 codechacha