LiveData と MutableLiveData の使い分け

observe 関係の場合は、できるだけ ViewModel と View(Activity 含む)の関係を疎にして、一方通行の関係がいいと思う。つまり、ViewModel 内部では MutableLiveData 扱いのデータを LiveData にして外側、つまり View 側に対してさらす形である。


private MutableLiveData<T> mData;
public LiveData<T> getData() {
    return mData;
}

一方、user action を ViewModel に入力する場合はどうするのがいいか?

この場合は、user action の影響を受ける LiveData は、View/Activity からの入力で変化することが当然であるから、上の observe の場合のように MutableLiveData を getter で隠蔽することは冗長である。なので、public な MutableLiveData を getter を介さずに直接さらしてしまえばいいと思う。


public MutableLiveData<T> mData;

そうして、当該 MutableLiveData に関する処理を、無理矢理 ViewModel に置かずに、素直に、View/Activity 内で処理を記述するのが、実は適切だと思う。

というのも、user action によって処理を分けたいのは、View/Activity 側の都合であって、ViewModel 側の場合ではないケースが考えられるからである。通常、user action を受け付けるのは、Activity が active すなわち、onResume から onPause の間のライフサイクルにおいてである。なので、その間のみ処理を行って、それ以外のタイミングでは処理を停止しているような形にするのが望ましい。そうなると、それらの処理の主となるコードは、Activity 側の各ライフサイクルに応じたメソッドに配置して、処理の結果変更を受ける変数のみ、MutableLiveData として ViewModel 内に所属させておくのがストレートなコード表現になるわけである。


サンプルアプリ

一例として、現在の年月日時分秒を表示する ViewModel パターンのサンプルアプリを作成してみた。単に ViewModel 側に保持されている年月日時分秒データを observe するだけなら一方通行の ViewModel パターンでいい。ところが、このサンプルでは、現在時をリアルタイムに表示したいため、Timer を使って 1 秒毎に ViewModel に保持されたデータを更新している。この処理を、アプリが onPause → onStop を経てバックグラウンドに回ってからも続けるのは、無駄以外の何物でもない。なので、Activity 側で(ViewModel に保持された)年月日時分秒データを、ライフサイクル的に active な場合にのみ 1 秒毎に一回処理するアルゴリズムにすることになる。つまり、Timer 処理は、ViewModel 側ではなく、Activity 側に記述している。

MainViewModel.Java

package com.scaredeer.intervalreload;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class MainViewModel extends ViewModel {
    private final MutableLiveData<String> mDatetime;
    public LiveData<String> getDatetime() {
        return mDatetime;
    }

    public final MutableLiveData<Boolean> isTimerActive;

    public MainViewModel() {
        mDatetime = new MutableLiveData<>(currentDatetime());
        isTimerActive = new MutableLiveData<>(false);
    }

    void postRefresh() {
        mDatetime.postValue(currentDatetime());
    }

    private String currentDatetime() {
        return Instant.ofEpochSecond(System.currentTimeMillis() / 1000L)
                .atZone(ZoneId.of("JST"))
                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
}
MainActivity.Java

package com.scaredeer.intervalreload;

import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProvider;

import android.os.Bundle;

import com.scaredeer.intervalreload.databinding.MainActivityBinding;

import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity {
    private MainViewModel mViewModel;
    private Timer mTimer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mViewModel = new ViewModelProvider(this).get(MainViewModel.class);

        MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
        binding.setLifecycleOwner(this);
        binding.setViewModel(mViewModel);
        binding.button.setOnClickListener(view -> {
            if (mViewModel.isTimerActive.getValue()) {
                stopTimer();
            } else {
                startTimer();
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (mViewModel.isTimerActive.getValue()) {
            startTimer();
        }
    }

    @Override
    protected void onPause() {
        if (mViewModel.isTimerActive.getValue()) {
            pauseTimer();
        }

        super.onPause();
    }

    private void startTimer() {
        stopTimer(); // スレッドの多重起動を防ぐため、
        // 既存のタイマーがあったとしたらちゃんと終了してから以下の処理に臨むようにする

        mTimer = new Timer();
        mTimer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                mViewModel.postRefresh();
            }
        }, 0, 1000L);

        mViewModel.isTimerActive.setValue(true);
    }

    // ユーザーの意思でタイマーを停止したので、当然、isTimerActive も false にトグルさせる。
    private void stopTimer() {
        pauseTimer();

        mViewModel.isTimerActive.setValue(false);
    }

    // 単なる pause の時は(復帰時にタイマーを再スタートする必要があるので)
    // isTimerActive は false にトグルさせない。
    private void pauseTimer() {
        if (Objects.nonNull(mTimer)) {
            mTimer.cancel();
        }
    }
}
main_activity.xml:

滅多矢鱈に Activity からコードを追い出して ViewModel に詰め込めばいいという話ではないという一例である。

コメント

このブログの人気の投稿

EP-805A 廃インク吸収パッド交換

m3u8 ファイルをダウンロードして ffmpeg で MP4 に変換・結合

WZR-HP-AG300H with OpenWrt