読者です 読者をやめる 読者になる 読者になる

Upボタンの実装メモ

はじめに

Upボタンの実装サンプルとかあまり詳しく書いてないので自分が実装したときのメモをまとめた。 正確性にあまり自信がない。

Upボタン

ActionBarの左端にはUpボタンと呼ばれる上位画面へ遷移するための機能がある。 ナビゲーションバーのBackボタンとは機能的にかなり異なる。 具体的な違いはAndroidDevelopersサイトの図を見るのが一番わかりやすいと思う。 http://developer.android.com/design/patterns/navigation.html

Upボタンの実装は難しい

Upボタンの実装のために、API level 16からActivityにいくつかのメソッドが追加され、SupportLibraryにも同様の機能を持つNavUtilsが追加された。 AndroidDevelopersサイトにもUp動作の実装例としてソースコードが記載されている。 http://developer.android.com/training/implementing-navigation/ancestral.html

しかし、ソースコード通りにUpボタンを実装してみても思ったとおりに動かないと感じることが多いかった。 この記事ではUpボタンの実装時に躓いたポイントと、その対処法をいくつか纏めることにする。

IllegalArgumentException

Upボタンを押した時、以下の様な例外が発生することがある。

    java.lang.IllegalArgumentException: Activity ACTIVITY_NAME does not have a parent activity name specified. (Did you forget to add the android.support.PARENT_ACTIVITY <meta-data>  element in your manifest?)

これは親アクティビティの指定漏れ。 親アクティビティの指定はJellyBean以降のためのandroid:parentActivityName属性と、それ以前のための<meta-data>の2箇所にわけて記載する必要がある。

        <activity
            android:name=".ChildActivity"
            android:parentActivityName=".ParentActivity">
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value=".ParentActivity" />
        </activity>

親アクティビティが再生成される

おそらく、Upボタン実装の難易度を高く感じさせているのはこの挙動だと思う。 サンプルコードの通りAndroidManifest.xmlPARENT_ACTIVITYの指定をして、以下のようにUpボタンの実装を行ったとする。

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                NavUtils.navigateUpFromSameTask(this);
                return true;
        }
        return super.onOptionsItemSelected(item);
    }

この状態で親Activityから子Activityへ遷移し、子ActivityでUpボタンを押下するとActivity#finish()して親Acitivityに戻るのではなく、全く新しい親Activityに遷移する。 このとき、upintentにはFLAG_ACTIVITY_CLEAR_TOPが含まれているため、親Activityが再生成されたように見えてしまう。 これは正常な動作だが、感覚的には親→子と移動したのだからひとつ前の親に戻ってほしいと思うのが普通だ。 解決方法はいくつかあるが、親ActivityのlaunchModesingleTopにするのが一番簡単。 Java側のソースコードは修正する必要がない。

        <activity
            android:name=".ChildActivity"
            android:launchMode="singleTop"
            android:parentActivityName=".ParentActivity">
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value=".ParentActivity" />
        </activity>

android:launchMode="singleTop"を指定した場合、スタックの最上位が親Activityの場合はインスタンスが再利用され、Activity#onCreate()の代わりにActivity#onNewIntent()が呼ばれることになる。

singleTopを避けたい場合

singleTopを指定できない、つまり親アクティビティが複数生成される可能性がある場合というのは、パラメータによって親Activityの内容が変わることが大半だ。 その場合、NavUtils.navigateUpFromSameTask()しただけではもともとパラメータが足りていない可能性が高い。 そういうときはUpボタン押下時の子ActivityでNavUtils.getParentActivityIntent()を使って親ActivityへのIntentを生成したあと、必要なパラメータをセットしてからNavUtils.navigateUpTo()する。 もちろん親は再生成されてしまうが、子同士の遷移によって親パラメータが変わる可能性もあるので仕方ない。 こういう場合は無理やり再生成を避けるよりはupintentのパラメータで復元できるようにしたほうが健全な気がする。

他のアプリから遷移した子ActivityでUpが動作しない

他のアプリのタスク上に子Activityがある場合、NavUtils.navigateUpFromSameTask()では親Activityに遷移しない。 他のアプリから呼ばれる可能性のあるActivityでは以下のように実装する必要がある。

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                Intent upIntent = NavUtils.getParentActivityIntent(this);
                if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
                    // 新しくタスクを生成する必要がある
                    TaskStackBuilder.create(this)
                            .addNextIntentWithParentStack(upIntent)
                            .startActivities();
                    finish();
                } else {
                    NavUtils.navigateUpTo(this, upIntent);
                }
                return true;
        }
        return super.onOptionsItemSelected(item);
    }

まとめ

大抵、実装よりもAndroidではUpとBackが別物ということから説明しなければならないのが面倒。 別物だけど単純に階層掘るだけのアプリならActivity#finish()呼べばそれで済むような気がしなくもない。 もうちょっと楽になってほしい。 ところで、Fragment遷移の場合のUpってどうするんだろう。