51
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

ViewModel

ViewModelは、内部的にはRetained Fragmentをうまく利用して、Activityが"本当にDestroy"されるまで居残り続けられる土台みたいなもの。

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

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

Slice.png

これ、何も考えずに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を使うと...

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

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();
      }
    });
  }

}
ViewModel
class ImageProcViewModel extends ViewModel {
  ObservableInt seekBarPosition = new ObservableInt(0);
}
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と完全におさらばすることができるではないか!!

(あとから気づいたけど、公式のリファレンスにもそれっぽいことが書いてあった

LiveData

LiveDataは、Activityが生きているときだけ動くリアクティブストリームみたいなもの。

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

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

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

Slice.png

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

Activity
class ImageProcessingActivity .... {
  private ActivityImageProcBinding binding;

  // いろいろ略

  private void updateImage() {
    showProgressDialog();

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


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

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

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

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

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

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;
    }
  }
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

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

Slice.png

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

まとめ

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?