View on GitHub
mixi-inc/AndroidTraining
ActivityとFragment
この章では、画面のライフサイクルの管理について解説します。

参考:Activities | Android Developers
参考:Tasks and Back Stack | Android Developers

目次

Android の画面の構成要素

Android の画面を作る上で重要なコンポーネントとして、下記のようなものがあります。

Activity
MVC の Controller に相当するものです。 画面のライフサイクルや、UI イベントの管理は Activity が受け持っています。
Fragment

再利用可能な UI コンポーネントのまとまりを管理します。Activity に組み込んで使用します。
こちらも、MVC の Controller に相当する責務を持っています。
様々なデバイスをサポートする上でも重要なコンポーネントとなります。

Fragment も Activity と同様ライフサイクルを持っていますが、Activity に組み込んで使う為、Activity のライフサイクルと連動するようになっています。

Layout・Widget
アプリのレイアウト作成で解説しました。

ライフサイクルの管理

Activity

Activity にはライフサイクルが存在します。 主には、画面が呼び出されてから、必要なくなってメモリから追い出される(破棄される)までの一連の流れをライフサイクルとして扱います。 以下に上げるライフサイクルの各状態は、ライフサイクルコールバックとして、Activity クラスに同じ名前で定義されています。

Activity Lifecycle

onCreate

Activity が一番最初にとる状態です。

Activity が起動し、画面を構成するまでの仕事をここで行います。
XML で定義したレイアウトを読み込んだり、View コンポーネントを取り出したりする処理を実行します。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // レイアウトを指定して、Activity がコントロールする View として扱うようにする
        setContentView(R.layout.activity_main);
    }
}
onStart(onRestart)
Activity の画面が構築され、ユーザに見える状態です。実際にユーザが UI に触れてインタラクションを実施出来るようになるまでの仕事をここで行います。
onResume
Activity の画面が構築され、ユーザが UI に触れてインタラクションを実施できる状態です。
onPause

ユーザが Activity を離れようとしている状態です。永続化するべき情報はこのタイミングで永続化を実施します。

この状態になった Activity へユーザが戻ると、onResume の状態へと遷移します。 よって、この状態になっても、必ずこの後の状態へ順に遷移して、Activity がメモリから破棄されるとは限りません。

onStop

Activity がユーザから見えなくなった状態です。

この状態になった Activity にユーザが戻ろうとした場合、onRestart の状態から onStart の状態へと遷移します。

onDestroy

Activity がシステムによって、メモリから追い出される直前の状態です。

この時点で、Activity に対するすべての参照を切っておかないと、メモリリークを起こします。
一方、システムの要求によって、アプリのプロセスごと kill された場合には、この状態になることなくアプリケーションが終了します。

Fragment

Fragment にもライフサイクルが存在します。

基本的には、Activity のライフサイクルと同じ状態を持っていますが、いくつか Fragment 特有の状態も持っています。
以下に上げるライフサイクルの各状態は、ライフサイクルコールバックとして、Fragment クラスに同じ名前で定義されています。

Fragment Lifecycle

Fragment は、Honeycomb で導入された新しいコンポーネントです。

Eclipse では、android.app.Fragment と android.support.v4.app.Fragment の 2 つがコード補完によって表示されますが、2.x 系で Fragment を使用する場合は、後者のパッケージのものを利用します。

また同時に、2.x 系で Fragment を組み込んだ Activity を作成する場合は、android.app.Activity ではなく、android.support.v4.app.FragmentActivity を継承するようにします。

onAttach

Fragment が Activity に組み込まれた状態です。
この時点で、Fragment が Activity に対して何らかのコールバックを提供する場合、Activity が必要なインタフェースを備えているかどうかチェックしておきます。

public class MainFragment extends Fragment {
    private FragmentCallbacks mCallback;
	
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        try {
            mCallback = (FragmentCallbacks) activity;
        } catch (ClassCastException e) {
            // Fragment が組み込まれる先の Activity に対して、FragmentCallbacks インタフェースの実装を要求する為
            // キャストに失敗した場合は、実行時例外としてプログラムのミスであることを示す 
            throw new IllegalStateException("activity should implement FragmentCallbacks", e);
        }
    }

    public static interface FragmentCallbacks {
        public void onHogehoge();
    }
}
onCreate

Fragment を構築する状態です。

Fragment がユーザに見える状態になるまで保持しておくべきコンポーネントの初期化を行います。

onCreateView

Fragment が持つ View を構築する状態です。

XML からレイアウトを取り出し、この Fragment の View として扱うための処理を行いますが、View を持たない Fragment を作ることも可能です。

public class MainFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // LayoutInflater を利用して、レイアウトをリソースとして読み込む
        View view = inflater.inflate(R.layout.fragment_main, container, false);
        return view;
    }
}
public class MainFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // View を持たない Fragment は、ここで null を返す
        return null;
    }
}
onActivityCreated
Activity の onCreate の状態の処理が終わったことを示す状態です。
onStart
Fragment の UI が構築され、ユーザに見える状態です。
onResume
Fragment の UI が構築され、ユーザとのインタラクションが出来るようになった状態です。
onPause

ユーザが別の画面への遷移をしようとして Fragment から離れていこうとした状態です。

Activity と同じく、この時点で、永続化するべき情報を保存するようにしておきます。
ただし、この状態になったからといって、必ずしもこの後の状態へ遷移し、Fragment がメモリから破棄されるわけではありません。

onStop
Fragment がユーザに見えない状態です。
onDestroyView

Fragment が扱う View などのコンポーネントに紐付いた各種リソースを開放するための状態です。

ここで Fragment への参照が View やコンポーネントに残っていると、メモリリークを起こします。
ユーザのナビゲーションや Fragment の操作等で、再びレイアウトにアタッチされる場合、onCreateView の状態へ戻ります。

onDestroy
Fragment が完全にメモリから破棄される直前の状態です。
onDetach
Fragment が Activity から切り離される状態です。

AndroidManifest での Activity の宣言

AndroidManifest は XML で記述されています。 Activity は、アプリケーションを構成するコンポーネントですので、 <application>要素の下にぶら下げる形で宣言します。

<activity>要素

<activity>要素には、どの Activity かを示したり、Activity のラベルを示したりする属性を付与できます。 以下に、代表的な属性を示します。

<activity
  android:name="string"
  android:label="string"
  android:exported=["true" | "false"]
  android:launchMode=["multiple" | "singleTop" | "singleTask" | "singleInstance"]
  android:configChanges=["mcc", "mnc", "locale", "touchscreen", "keyboard", "keyboardHidden", "navigation", "screenLayout", "fontScale", "uiMode", "orientation", "screenSize", "smallestScreenSize"]
  android:theme="resource or theme"
  android:uiOptions=["none" | "splitActionBarWhenNarrow"]
  android:windowSoftInputMode=["stateUnspecified", "stateUnchanged", "stateHidden", "stateAlwaysHidden", "stateVisible", "stateAlwaysVisible", "adjustUnspecified", "adjustResize", "adjustPan"]>
</activity>
要素 意味
android:name Activity のクラス名。FQDN か、<application>要素で宣言したパッケージ名から後ろのパス表現で記述する
android:label Activity のラベル。ユーザに見えるものなので、分かりやすいものにしておく
android:exported 他のアプリからも呼び出すことができるようにするかどうか。デフォルトではfalseになるが、例外的に、<intent-filter>要素を子要素に持つ<activity>は、この属性がデフォルトでtrueになることに注意。特に理由がない限りは、falseを明示すること
android:configChanges Activity が自身で制御するコンフィギュレーション変化の一覧。複数指定が可能で、パイプで繋ぐ
android:theme Activity に適用するテーマ
android:uiOptions Activity に適用する、UI にまつわる設定。現状は ActionBar の設定のみが存在している
android:windowSoftInputMode ソフトウェアキーボードと Activity の表示に関する調整要素。ソフトウェアキーボードを自動で表示するかどうかや、Activity の拡大・縮小についての振る舞いを決める

ランチャーから起動する Activity の宣言

アプリを起動する時、ランチャーは、当該アプリに対して、暗黙的 Intent を発行します(Intent は、Android の各種コンポーネントを結びつけるためのメッセージオブジェクトです。新しい Activity を呼び出すときにも、この Intent を利用しています)。 この暗黙的 Intent を受け付けることの出来る Activity の宣言の例を以下に示します。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.mixi.sample"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <!-- Activity に関する宣言 -->
        <!-- 新しい Activity を作ったら、必ずこの宣言を追加する -->
        <!-- android:name には、FQDN か、jp.mixi.sampleの続きからのパスを入れる -->
        <activity
            android:name="jp.mixi.sample.MainActivity"
            android:label="@string/app_name" >
            <!-- ランチャーから呼び出す対象とするための宣言 -->
            <!-- ランチャーから呼び出さない Activity であれば、この宣言は不要 -->
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

<activity>要素の子要素として、<intent-filter>要素を宣言します。 この<intent-filter>要素に更に子要素として、Intent に付与される Action と Category を指定します。

ランチャーからの呼び出しでは、android.intent.action.MAIN というアクションと、android.intent.category.LAUNCHER というカテゴリが付与されますので、これを<intent-filter>要素の子要素に宣言しておきます。

Activity とレイアウトの管理

Activity が管理するレイアウトの指定

Activity#onCreate(Bundle)の中で、Activity#setContentView(int)を呼び出します。
呼び出すメソッドの引数に渡す int 型整数は、レイアウトファイルの id です。
レイアウトファイルの id は、R クラスで管理されており、レイアウトファイルを作成すると自動で生成されます。

public class MyActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // activity_main.xml をこの Activity のレイアウトとして管理する場合
        setContentView(R.layout.activity_main);
    }
}

レイアウトからの View の取得

View にandroid:idが割当てられていれば、この id を元に、View の取得をすることが出来ます。
id を元に View を取得するメソッドに、Activity#findViewById(int)があります。

<TextView
    android:id="@+id/MyTextView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
public class MyActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // findViewById の引数に、View に割り当てた id を指定する
        // View オブジェクトが返ってくるので、適宜キャストする
        TextView view = (TextView) findViewById(R.id.MyTextView);
    }
}

Fragment の組み込み

2 種類の組み込み方があります。

レイアウトに記述する

Activity で使うレイアウトの XML の中で、<fragment>タグを利用して記述する方法です。

もっとも単純な Fragment の使い方です。

レイアウトで使用する各種属性の他に、android:id属性と、android:name属性を必ず指定します。
android:name属性は、Fragment のクラス名を FQDN で記述します。

<!-- Activity 用のレイアウトでの Fragment の宣言。andorid:id によって、状態管理を行なっている -->
<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"

        <!-- 何かしらの他の Viewたち... -->

        <!-- fragment の宣言。android:name に指定する Fragmentの名前は FQDN で書く。 -->
        <fragment
                android:id="@+id/MyFragment"
                android:name="jp.mixi.sample.fragment.MyListFragment"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
</LinearLayout>

レイアウトに定義された Fragment を Activity で取り出すには、FragmentManager を利用します。

public class MyActivity extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // FragmentManager も、android.support.v4.app.FragmentManager を利用する
        FragmentManager manager = getSupportFragmentManager();
        // レイアウトから Fragment を見つけ出してインスタンスを得る
        Fragment fragment = manager.findFragmentById(R.id.MyFragment);
    }
}

FragmentManager と FragmentTransaction で管理する

画面(Activity)の中で、動的に Fragment を切り替える場合を考えると、レイアウトに書いてしまうと実現が難しくなります。
そこで、Activity の中で動的に Fragment を切り替える仕組みも用意されています。

動的に Fragment を切り替える場合は、Fragment を保持するためだけの Layout を用意しておく必要があります。

<!-- Activity 用のレイアウトでの Fragment の宣言。andorid:id によって、状態管理を行なっている -->
<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"

        <!-- 何かしらの他の Viewたち... -->

        <!-- Fragment を動的に入れ替えるための入れ物用のレイアウト -->
        <FrameLayout
            android:id="@+id/FragmentContainer"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
</LinearLayout>

FragmentManager で、FragmentTransaction を開始し、この Transaction の中で、動的に Fragment の切り替えを行います。

public class MyActivity extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FragmentManager manager = getSupportFragmentManager();
        // FragmentTransaction を開始
        FragmentTransaction transaction = manager.beginTransaction();

        // FragmentContainer のレイアウトに、MyFragment を割当てる
        transaction.add(R.id.FragmentContainer, MyFragment.createInstance());

        // FragmentContainer のレイアウトの中身を、MyFragment に置き換える
        transaction.replace(R.id.FragmentContainer, MyFragment.createInstance());

        // Fragment を削除する
        transaction.remove(mFragment);

        // 変更を確定して FragmentTransaction を終える
        transaction.commit();
    }
}

Fragment の作成の注意点

Fragment のクラスを作成するときには、以下の点に注意してください。

空のコンストラクタを定義する
何もしなくても、空のコンストラクタが必要です。これは、Android のフレームワークが、リフレクションによって、Fragment のデフォルトコンストラクタを呼び出すためです。
public class MyFragment extends Fragment {
    public MyFragment() {}
}
Fragment のインスタンス化にはコンストラクタを使わない

Fragment を動的に管理する際に必要なノウハウです。
Fragment のインスタンス化の際、Fragment に対して、引数を何かしら渡したい場合がありますが、このためにコンストラクタや setter メソッドを使ってはいけません。
これは、Android のフレームワークが、Fragment の状態の復旧時に、デフォルトコンストラクタを呼び出すためです。つまり、setter メソッドを定義しても、フレームワークは全くその存在を気にすることが出来ない為、setter メソッドも意味を成さなくなります。

代わりに、Fragment の初期化のために、専用の static メソッドを用意し、引数を渡す場合は、`Fragment#setArguments(Bundle)`を利用して、Bundle に引数として渡したいデータを詰め込みます。

public class MyFragment extends Fragment {
    public MyFragment() {}

    // 初期化専用のメソッド。Fragment に対する初期化用の引数はここで管理する
    public static Fragment createInstance(int hoge) {
        Fragment fragment = new MyFragment();
        // Fragment に渡す引数を詰めこむオブジェクト
        Bundle args = new Bundle();
        args.putInt("hoge", hoge);

        // 詰め込んだオブジェクトを Fragment に渡す
        fragment.setArguments(args);

        // 新しいインスタンスを返す
        return fragment;
    }
}

タスクとバックスタック

Android には、タスクとバックスタックという考え方があります。 この考え方が取り扱うのは、複数の Activity の管理です。 同一アプリ内で扱う複数の Activity だけでなく、他のアプリとの連携も、タスクとバックスタックの扱う範囲です。

タスク

タスクとは、ユーザがアプリを利用して何らかの操作を行う単位です。このことから、Linux などで用いられるタスクとは異なる意味を持っています。

1つのタスクには、起動した Activity のまとまりをスタックで持つバックスタックが割当てられます。

バックスタック

バックスタックとは、Activity の呼び出し順を管理するスタックです。 アプリを起動すると、最初にランチャーから呼び出される Activity がバックスタックに積まれます(push)。

その Activity から新しい別の Activity を呼び出すと、その Activity がバックスタックに積まれます(push)。 新しく起動した Activity で、端末のバックキー(戻るボタン)を押すなどして前の Activity に戻ると、バックスタックに積まれていた Activity は破棄されます(pop)。

すべての Activity がバックスタックから居なくなると、アプリケーションは終了することとなります。

Back Stack Visualization

マルチタスク

あるタスクのバックスタックに Activity が積まれている状態で、ユーザがホームキーで端末のホーム画面に戻った場合、タスクはバックグラウンドで待機状態となります。

この時、別のアプリを起動すると、新たな別のタスクが定義され、新しいタスクのバックスタックに起動したアプリの Activity が積まれます。 つまり、新しいタスクはフォアグラウンドで実行中の状態となります。

ここで、ユーザが再びホームキーでホーム画面に戻り、最初に利用していたアプリを起動してバックグラウンドに居たタスクをフォアグラウンドへ遷移させると、もう一方のフォアグラウンドに居たタスクがバックグラウンドへ移ります。

Multi-Task Visualization

これが Android のマルチタスクです。

マルチタスクで多数のタスクをバックグラウンドへ移すと、その分だけメモリを消費します。メモリ残量が少なくなると、システムは、バックグラウンドに居るタスクのなかから、優先順位を見てタスクの持つ Activity の破棄を試みます。

Activity が破棄されても、破棄される前の状態に復元できるよう、Activity のライフサイクルにしたがって実装をしておくべきです。

状態の保存と復旧

Activity には、以下の二つのコールバックメソッドが用意されており、これらを活用して、状態の保存と復旧を行います。

onSaveInstanceState
Activity が破棄される前に、状態を保存するために呼ばれるコールバックメソッドです。 引数に Bundle オブジェクト(Android 独自のコレクション・フレームワーク)が渡されるので、このオブジェクトに状態を保存します。
onRestoreInstanceState
Activity が再度フォアグラウンドに遷移する際、状態を復旧するために呼ばれるコールバックメソッドです。 引数に Bundle オブジェクトが渡されます。この Bundle オブジェクトには、onSaveInstanceState() メソッドで保存した状態が入っていますので、その状態を取り出して復旧します。 Activity の onCreate() メソッドの引数にある Bundle オブジェクトも同じ役割を持っています。

同様、Fragment にも onSaveInstanceState() メソッドが定義されており、ここで状態の保存をすることができます。

一方、状態の復旧には、onCreate() や onCreateView()、onActivityCreated() メソッドの引数にある Bundle オブジェクトを利用します。

Launch Mode

Activity のインスタンスをどのようにスタック上で管理するかを決めるモードです。
このモードの宣言は、AndroidManifest の <activity>要素で行います。

<activity
    ...
    android:launchMode="singleTop"/>
モード名 意味
standard デフォルトのモード。Activity の呼び出しごとに毎回インスタンスを生成する為、複数の同じActivityのインスタンスがスタック上に現れる。
singleTop Activity を呼び出した時、スタックの一番上にその Activity のインスタンスがあるときは、Activity#onNewIntent()を呼び出して、そこに新しい Intent を送りつける
singleTask 新しいタスクのルートとして Activity のインスタンスを生成するが、既にタスクのスタックにインスタンスがある場合は、Activity#onNewIntent()を呼び出して、新しいタスクのルートに据える
singleInstance タスクの中で、Activity のインスタンスは常に 1 つで、かつそのインスタンスが属するタスクの中では、他の Activity を起動させない

殆どの場合、standard モードないしは singleTop モードを利用することになります。
公式のリファレンスにも書かれている通り、ほとんどのアプリケーションにおいて、singleTaks や singleInstance は不適切なモードです。

実習・課題

実習

Activity

  1. 新しい Activity と、それに対応するレイアウトを作成してください。
    (使用するプロジェクト: AndroidStudio/practice/fundamentals/2nd/ActivityPractice/practice1/app/build.gradle)
  2. 1で作った Activity を、アプリ起動時に表示するよう変更してください。 (変更先のプロジェクト: AndroidStudio/practice/fundamentals/2nd/ActivityPractice/practice2/app/build.gradle)

Fragment

  1. 新しい Fragment と、それに対応するレイアウトを作成してください。
    (使用するプロジェクト: AndroidStudio/practice/fundamentals/2nd/FragmentPractice/practice1/app/build.gradle)
  2. 1で作った Fragment を、既存の Activity に組み込んでください。 (変更先のプロジェクト: AndroidStudio/practice/fundamentals/2nd/FragmentPractice/practice2/app/build.gradle)

課題

  1. Toast#makeText(Context, int, int)メソッド(仕様)を使って、Activity のライフサイクルのコールバックメソッドが、どのような順番で実行されるかを画面に表示し、列挙してください。
    (使用するプロジェクト: AndroidStudio/assignments/fundamentals/2nd/ControllerAssignment1/build.gradle)
  2. Activity が 2 つ用意されています。起動後の Activity から呼び出された Activity で、状態管理を適切に行なってください(ControllerLifecycleAssignment2 プロジェクトを使う)。
    (使用するプロジェクト: AndroidStudio/assignments/fundamentals/2nd/ControllerAssignment2/build.gradle)
  3. activity_main.xmlから、Fragment に切り出すものを適宜選択し、Fragment 化してください。
    (使用するプロジェクト: AndroidStudio/assignments/fundamentals/2nd/ControllerAssignment3/build.gradle)
  4. 課題 3 で切り出した Fragment を用いて、新しい画面を作成してください(課題 3 と同じプロジェクトの中で実施する)。
  5. Activity で発生しているメモリリークを特定し、解消してください。
    (使用するプロジェクト: AndroidStudio/assignments/fundamentals/2nd/ControllerAssignment4/build.gradle)