Android Architecture ComponentsのViewModelとLiveDataを使えば画面回転も怖くない

ViewModelとかLiveDataのサンプルってAPI通信とかのわりと複合的なのが多いけど、もっと単純に使っていけるんではないか、という話。
ほぼ自分用のメモ。

## ViewModel

[ViewModel](https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html)は、内部的にはRetained Fragmentをうまく利用して、Activityが"本当にDestroy"されるまで居残り続けられる土台みたいなもの。

<img src="https://qiita-image-store.s3.amazonaws.com/0/74571/fd228aaf-62c5-18a9-1a82-9b4169f5ab2f.png" height=320 />

これ、データバインディングを使うとだいぶありがたみがある。

### たとえば、画像処理をやるアプリだと...

![Slice.png](https://qiita-image-store.s3.amazonaws.com/0/74571/70664982-7a48-0866-b24c-ff7c4e6b66a2.png)

これ、何も考えずにActivityとsavedInstanceStateでやろうとすると結構めんどくさい。

```java
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を使うと...

双方向バインディングとかイケイケなものを使わない前提でも

```java:Activity
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();
      }
    });
  }

}
```

```java:ViewModel
class ImageProcViewModel extends ViewModel {
  ObservableInt seekBarPosition = new ObservableInt(0);
}
```

```xml:res/layout/activity_image_proc.xml
<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と完全におさらばすることができる**ではないか！！

(あとから気づいたけど、公式のリファレンスにも[それっぽいこと](https://developer.android.com/topic/libraries/architecture/saving-states.html)が書いてあった

## LiveData

[LiveData](https://developer.android.com/topic/libraries/architecture/livedata.html)は、Activityが生きているときだけ動くリアクティブストリームみたいなもの。

これもまた、ViewModelが管理してる状態をActivity側から参照するときに有用である。

### あらためて、画像処理をやるアプリだと...

画像処理に時間がかかるので、くるくる表示をしたいとする。

![Slice.png](https://qiita-image-store.s3.amazonaws.com/0/74571/c00bcdb2-69b1-8c21-6247-812d9c046706.png)

ユーザがちゃんとくるくる表示が消えてから画面回転してくれたら、何も問題はないのだけど、ユーザはそんなには優しくない。くるくる表示中に画面回転する人がいるかもしれない。

```java:Activity
class ImageProcessingActivity .... {
  private ActivityImageProcBinding binding;

  // いろいろ略

  private void updateImage() {
    showProgressDialog();

    processImageAsync()
      .setOnCompleteListener(new OnCompleteListener(){
          public void onComplete() {
            hideProgressDialog();
          }
      });
  }


```

割と有名な話だけど、こんなかんじでくるくるの状態管理をActivityでやってしまうと、画面回転されたときに詰んでしまう。

### でも「くるくる表示」制御ロジックをビューモデルには移せない！

「画面回転でプログレスの状態は引き継ぎたいので、ViewModelにロジックをもたせよう！」というのは容易に思いつく。しかし、show/hideProgressDialogはActivityが持っている必要があって、単純なロジック移行は難しそうに見える。

"処理が始まる前から、処理を終えるまでダイアログを表示" という発想だとロジックの分解が難しいので、まずはちょっとロジックをリファクタリングしてみる。

```java: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側に処理を映す。

```java:Activity
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;
    }
  }
```

```java:ViewModel
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のコールバックが画面回転の後にある場合

![Slice.png](https://qiita-image-store.s3.amazonaws.com/0/74571/62b90153-c0a5-e83f-12cd-c7ca1639e127.png)

#### SUCCESSのコールバックが画面回転の途中にある場合

![Slice.png](https://qiita-image-store.s3.amazonaws.com/0/74571/54faaf83-b8b6-979a-7f25-5c036e82fdf4.png)

これで無事にプログレスダイアログの表示を画面回転をまたいでやることができた。

## まとめ

* ViewModelはデータバインディングと非常に相性がよい
    * 画面回転でいちいちsavedInstanceStateにごにょごにょやらなくて済む
* ViewModelの状態監視にはLiveDataが便利
    * 画面回転をまたいで状態をビューに反映することが簡単にできる