ViewModelとかLiveDataのサンプルってAPI通信とかのわりと複合的なのが多いけど、もっと単純に使っていけるんではないか、という話。
ほぼ自分用のメモ。
ViewModel
ViewModelは、内部的にはRetained Fragmentをうまく利用して、Activityが"本当にDestroy"されるまで居残り続けられる土台みたいなもの。
これ、データバインディングを使うとだいぶありがたみがある。
たとえば、画像処理をやるアプリだと...
これ、何も考えずにActivityとsavedInstanceStateでやろうとすると結構めんどくさい。
class ImageProcessingActivity .... {
private int seekBarPosition;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_proc);
if (savedInstanceState != null) {
seekBarPosition = savedInstanceState.getInt("seekBarPosition");
}
// 中略...
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onProgressChanged (SeekBar seekBar, int progress, boolean fromUser) {
seekBarPosition = progress;
updateImage();
}
});
}
private void updateImage() {
// seekBarPositionの値をもとに画像処理をする
}
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("seekBarPosition", seekBarPosition);
}
}
seekBarPositionを復元するだけでもこんな感じで、これにさらにBitmapの状態もちゃんと復元しようとするとわりと骨が折れる。
ViewModel+DataBindingを使うと...
双方向バインディングとかイケイケなものを使わない前提でも
class ImageProcessingActivity .... {
private ActivityImageProcBinding binding;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_image_proc);
binding.viewModel = ViewModelProviders.of(this).get(ImageProcViewModel.class);
// 中略...
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onProgressChanged (SeekBar seekBar, int progress, boolean fromUser) {
binding.seekBarPosition.set(progress);
updateImage();
}
});
}
}
class ImageProcViewModel extends ViewModel {
ObservableInt seekBarPosition = new ObservableInt(0);
}
<layout>
<data>
<variable
name="viewModel"
type="... ImageProcViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
...
<SeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progress="@{viewModel.seekBarPosition}
ObservableFieldとかその辺の予備知識は必要になってくるものの、savedInstanceStateと完全におさらばすることができるではないか!!
(あとから気づいたけど、公式のリファレンスにもそれっぽいことが書いてあった
LiveData
LiveDataは、Activityが生きているときだけ動くリアクティブストリームみたいなもの。
これもまた、ViewModelが管理してる状態をActivity側から参照するときに有用である。
あらためて、画像処理をやるアプリだと...
画像処理に時間がかかるので、くるくる表示をしたいとする。
ユーザがちゃんとくるくる表示が消えてから画面回転してくれたら、何も問題はないのだけど、ユーザはそんなには優しくない。くるくる表示中に画面回転する人がいるかもしれない。
class ImageProcessingActivity .... {
private ActivityImageProcBinding binding;
// いろいろ略
private void updateImage() {
showProgressDialog();
processImageAsync()
.setOnCompleteListener(new OnCompleteListener(){
public void onComplete() {
hideProgressDialog();
}
});
}
割と有名な話だけど、こんなかんじでくるくるの状態管理をActivityでやってしまうと、画面回転されたときに詰んでしまう。
でも「くるくる表示」制御ロジックをビューモデルには移せない!
「画面回転でプログレスの状態は引き継ぎたいので、ViewModelにロジックをもたせよう!」というのは容易に思いつく。しかし、show/hideProgressDialogはActivityが持っている必要があって、単純なロジック移行は難しそうに見える。
"処理が始まる前から、処理を終えるまでダイアログを表示" という発想だとロジックの分解が難しいので、まずはちょっとロジックをリファクタリングしてみる。
class ImageProcessingActivity .... {
private ActivityImageProcBinding binding;
// いろいろ略
private void updateImage() {
updateInternalState(IN_PROGRESS);
processImageAsync()
.setOnCompleteListener(new OnCompleteListener(){
public void onComplete() {
updateInternalState(SUCCESS);
}
});
}
private void updateInternalState(InternalState state) {
if (state == IN_PROGRESS) {
showProgressDialog();
} else {
hideProgressDialog();
}
if (state == SUCCESS) {
onSuccess();
resetInternalState();
return;
}
if (state == FAILURE) {
showError();
resetInternalState();
return;
}
}
private void resetInternalState() {
updateInternalState(INIT);
}
ざっくりいうと 「プログレスダイアログは、 InternalState.IN_PROGRESS
をビューに写像しているだけだ!」という発想の転換をしただけだ。
LiveData+ViewModelの構成にする
さて、本題。ViewModel側に処理を映す。
class ImageProcessingActivity .... {
private ActivityImageProcBinding binding;
private ImageProcViewModel viewModel;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_image_proc);
viewModel = ViewModelProviders.of(this).get(ImageProcViewModel.class);
binding.viewModel = viewModel;
// ビューモデル側で持っているInternalStateの値に応じて表示を変える
viewModel.internalStateLiveData().observe(this, new Observer<InternalState>() {
public void onChange(@Nullable InternalState state) {
updateInternalState(state);
}
});
}
// いろいろ略
private void updateInternalState(InternalState state) {
if (state == IN_PROGRESS) {
showProgressDialog();
} else {
hideProgressDialog();
}
if (state == SUCCESS) {
onSuccess();
viewModel.resetInternalState();
return;
}
if (state == FAILURE) {
showError();
viewModel.resetInternalState();
return;
}
}
class ImageProcViewModel extends ViewModel {
ObservableInt seekBarPosition = new ObservableInt(0);
private MutableLiveData<InternalState> internalStateLiveData = new MutableLiveData<>();
public LiveData<InternalState> internalStateLiveData() {
return internalStateLiveData;
}
public void updateImage() {
updateInternalState(IN_PROGRESS);
processImageAsync()
.setOnCompleteListener(new OnCompleteListener(){
public void onComplete() {
updateInternalState(SUCCESS);
}
});
}
private void updateInternalState(InternalState state) {
internalStateLiveData.setValue(state);
}
private void resetInternalState() {
updateInternalState(INIT);
}
}
LiveDataは画面が生きていない時にはコールバックをせず、画面が生き返ってから改めて最新の値を通知してくる。
一見奇妙な動作だけども、この動作によってSUCCESSが画面回転の途中にあろうと画面回転の後にあろうと、コードを書き分けること無くうまく動いてくれるのだ!
SUCCESSのコールバックが画面回転の後にある場合
SUCCESSのコールバックが画面回転の途中にある場合
これで無事にプログレスダイアログの表示を画面回転をまたいでやることができた。
まとめ
- ViewModelはデータバインディングと非常に相性がよい
- 画面回転でいちいちsavedInstanceStateにごにょごにょやらなくて済む
- ViewModelの状態監視にはLiveDataが便利
- 画面回転をまたいで状態をビューに反映することが簡単にできる