Android の罠 [1] ちゃんと onSaveInstanceState する

  • 169
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

ちょくちょく Android ハマることとか気をつけることとかを書いてみます。Android てんでダメなので誤ったことを書いていたら指摘いただければ。

Activity と Fragment は死にやすい

Activity と Fragment, 画面を表示している間は生きてくれるものと普通の脳みそなら考えてしまいます。
が、Android ではそうではありません。

まず、画面を回転させるだけで死にます。(作り直される)
アプリがバックグラウンドに居る間に知らない間に殺されたりもします。(まあ iOS もそうだけど)
同じ画面なのに画面を回転させるだけで onCreate が二回発火します。

何らかの処理のフローとかを状態変数で管理していると知らない間におかしくなったりします。(そもそも何時死ぬか分からない Activity に状態変数を管理させるのもよくない)
コールバック云々の話もしたほうが良い気がしますが、今回は触れません。
(POST をお願いされたのに、戻ったら Activity が死んでいた…ウッ頭が)

状態の保存は、onCreate(Bundle savedInstanceState)onSaveInstanceState(Bundle outState) をちゃんと実装していれば問題を回避出来ます。

どう実装するか

ここで保存と復元を考えておくべきものは次の 3 つです

  1. 状態変数
  2. テキストフィールドの入力内容とか
  3. ビューの状態

1. 状態変数

変数の保存と復元は Icepick っていうナイスなライブラリがあるので任せてしまいましょう。

(2 タブにしてます)

class ExampleActivity extends Activity {
  @Icicle String username; // This will be automatically saved and restored

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Icepick.restoreInstanceState(this, savedInstanceState);
  }

  @Override protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Icepick.saveInstanceState(this, outState);
  }
}

@Icicle アノテーションをプロパティに付けて、よろしくやるだけでイイ感じに保存と復元してくれます。

かんたん。

2. フォームの内容

次にフォーム入力内容の保存と復元ですがこちらは少し手間が必要です。
getViewById() で View を取ってきて .getText().toString() したものを保管します。getView を介在させる分面倒くさいです。
なぜか savedInstanceStatenull で返るクソ設計らしいので null チェック必要です。

Icicle ではよろしくやってくれていたのですが、本来はそれぞれの変数にキーが必要なので、適当に対応付けします。(EditText に対して EDIT_TEXT みたいな)

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

  if (savedInstanceState == null) {return;} // めんどくさい
  String editTextValue = savedInstanceState.getString("EDIT_TEXT"); // キー考えるのめんどくさい
  if (editTextValue != null) { editText.setText(editTextValue); } // null チェックだるい (追記: 今は setText() 内で null を "" に置換してくれる模様)
}

@Override protected void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);
  outState.putString("EDIT_TEXT", editText.getText().toString());
}

代案?

だるさがヤバいので Icicle property を介在させてこんな感じにすると良いのかもしれません。

@InjectView(R.id.edit_text) editText;
@Icicle String editTextValue;

@Override protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_my);
  ButterKnife.inject(this);

  Icepick.restoreInstanceState(this, savedInstanceState);
  editText.setText(editTextValue);
}

@Override protected void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);

  editTextValue = editText.getText().toString();
  Icepick.saveInstanceState(this, outState);
}

順番に気をつけましょう。変数が無駄に増えて、あまりスマートなやり方ではないと思います。
本来ならば editTextValue と EditText の内容を RxJava みたいなので bind するとかそういうのするともう少し綺麗になります。

Icepick と ButterKnife みたいに簡単にイイ感じにやってくれるライブラリが待たれますね。(@IcicleView みたいな)

3. View の状態

最後の View の状態の復元です。
これは、ユーザの操作によって状態の変わっている View をキチンと復活させるとかそういう話です。(ボタン押したら数値が増えるカウンターとか)

これは Icicle でイイ感じに保管した状態変数をほげほげして View を再現すればよいです。
難しいことはないですが、意識しなければ画面を回転させるだけで状態がリセットされる香ばしいアプリになります。

それぞれの状態変数が null かどうかで分岐する必要もあり面倒くさいです。
このへんは、RxJava とかで MVVM 的にやれば綺麗になると思いますが今回は触れません。

次?

何回か出た MVVM とか Rx まわりを触れたい。
でも次は多分 UI Thread と Background Thread まわりのアレコレにすると思います。

なんか間違ってたらコメントとか @_kaiinui にメンションお願いします