Activityインスタンス破棄対策にIcepickを使ってみる

2016/02/18に開催されたDroidKaigi2016の基調講演で紹介されたライブラリを試そう！ということでとっかかりがよかった「Icepick」を触ってみました。
このライブラリで何ができるか、どういうケースで使えるかが伝われば幸いです。

[DroidKaigi2016 1日目の基調講演のスライド]OSSの動向を捉えた実装方針
https://speakerdeck.com/wasabeef/day1-keynote-in-droidkaigi-2016

# Icepickとは?
ActivityなどのUI系オブジェクトのフィールドの保存と復元部分の実装を簡単にするためのライブラリです。これを使えば面倒な「Activityインスタンス破棄問題」をスマートに解決出来ることが期待できます。

https://github.com/frankiesardo/icepick

執筆時点での最新バージョンは3.1.0です。

# ここでいう「Activityインスタンス破棄問題」は何が問題か?
詳細は別の記事で書こうかと考えていますが、ざっくり言うと…

・端末が回転したり、言語設定を変えたりした場合
・メモリが足りなくなったりした場合
にActivityインスタンスが破棄されます。その時に保存しておいてほしい値がなくなっているため、状態がリセットされてしまう。

というものです。（この記事読んでいる時点でこの問題の重大さはわかると思いますが…）

# 使い方
ここからは使い方について解説していきます。

使っているサンプルコードはGithubにアップロードしています。
https://github.com/LyricalMaestro/IcepickSample

## 下準備（build.gradleの変更）
appフォルダ直下のbuild.gradleに以下のコードを追記してください。

```java:build.gradle
android{

  repositories {
    maven {url "https://clojars.org/repo/"}
  }

  //…略
}

dependencies {
  compile 'frankiesardo:icepick:3.1.1-SNAPSHOT'
  provided 'frankiesardo:icepick-processor:3.1.1-SNAPSHOT'
}
```

## その１. Activity/Fragmentのフィールドインスタンスを保存・復元する

まず、保存したいフィールドに`@State`をつけます。そして、onSavedInstanceStateメソッド内にIcepick#onSavedInstanceStateを、onCreateメソッド内にIcepick#restoreInstanceStateを実行するようにします。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity {

    private TextView mMsgTextView;

    @State
    String mMessages = "";
    @State
    int mCount = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Icepick.restoreInstanceState(this, savedInstanceState);

        //略
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Icepick.saveInstanceState(this, outState);
    }
    // 以下略
}
```

これだけです。ちなみにIcePickを使わないと以下のような感じになります。onCreate内が特にぐちゃっとしていますし、keyの打ち間違えによる不具合も発生しそうですよね。

```java:MainActivity.java(Icepickなし)
public class MainActivity extends AppCompatActivity {

    private TextView mMsgTextView;

    @State
    String mMessages = "";
    @State
    int mCount = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(savedInstanceState != null){
            mMessages = savedInstanceState.getString("STRING");
            mCount = savedInstanceState.getInt("INTEGER");
        }

        //略
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString("STRING", mMessages);
        outState.putInt("INTEGER", mCount);
    }
    // 以下略
}
```

## その2. カスタムViewのフィールドインスタンスを保存・復元する
カスタムViewの場合もその1と同様に、保存したいフィールドに`@State`をつけます。そして、onSavedInstanceStateメソッド内にIcepick#onSavedInstanceStateを、onCreateメソッド内にIcepick#restoreInstanceStateを実行するようにします。

```java:CustomView.java
public class CustomView extends View {

    @State
    int mColorIndex = 0;

    public CustomView(Context context) {
        super(context);
        init();
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    public Parcelable onSaveInstanceState() {
        return Icepick.saveInstanceState(this, super.onSaveInstanceState());
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(Icepick.restoreInstanceState(this, state));
        changeColor();
    }

    //以下略
}
```

## その3. Activityが参照するTextViewなどの標準Viewの状態を保持するには?
その1でやったようなノリで参照するEditTextが持つ文字列も保存・復元の対象にしたいと思うかもしれませんが、現状できません。なので、このようなことをする場合には一度String型のフィールドに値を退避させてからそのフィールドを保存・復元みたいなことをしないといけません。（この辺りが少しめんどくさいところではありますが…）

```java:これはできない
public class MainActivity extends AppCompatActivity {

    @State
    TextView mMsgTextView;

    // 以下略
}
```

# 注意点
Icepickはたくさんのライブラリに依存しています。（以下はIcepickインポート時に芋づる式にインポートされたライブラリたち）
![スクリーンショット 2016-02-20 15.45.19.png](https://qiita-image-store.s3.amazonaws.com/0/14989/c605670a-11a2-0dea-33e5-9e3134d8b571.png)

メソッド数とかに余裕がない場合は注意が必要かもしれません。。guavaとか入っている段階でかなりのメソッド数いきそうですが…

# 総括
少しめんどくさいところもありますが、保存すべきフィールドがたくさんある場合にはこれを使うとコードがすっきりする+バグの温床がなくなる、というので恩恵を受けられると思います。