Help us understand the problem. What is going on with this article?

Androidの画面設計や遷移に関して整理してみる

More than 3 years have passed since last update.

AndroidのActivityやFragmentでの画面設計や遷移に関して分かりづらいので整理してみます。
(というか情報が分散しすぎていたので1つにまとめたかった)
間違っていたらご指摘いただけたらと思います。

もしよかったらこちらも合わせてどぞー
最近のAndroidネイティブ開発まとめ(2017/4版)
Androidのライフサイクルからアプリ設計を見直してみる

画面の構成要素

まず画面の構成要素から

  • Activity:画面の構成要素、生成から破棄まで一連のライフサイクルを持つ
  • Fragment:Activity内部に格納できる入れ子の画面、Fragmentも生成から破棄まで一連のライフサイクルを持つ、FragmentのライフサイクルはActivityのライフサイクルに追従する
  • ルートActivity:アプリの起動直後に呼ばれるActivity
  • Activity Stack:Activityを格納するスタック
  • タスク:ユーザーがあるアプリケーションを使ったとして、その状態遷移に含まれるアクティビティの集合をタスクという。イメージとしては1つのスタックに対し、アプリケーション単位にActivityを管理するタスクが存在する。

参考:アクティビティ と タスク と スタック と 起動モード (と ライフサイクル)

ルートアクティビティの判別:
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
が入っているのがルートアクティビティ

AndroidManifest.xml
        <activity
            android:name="(パッケージ名).MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

ActivityのlaunchModeに関して

contextのstartActivityメソッドもしくはstartActivityForResultメソッドを利用することでActivityやFragmentから別のActivityを呼び出すことができます。
この際、ActivityのlaunchModeを指定することでActivityをどのタスクに保持するか、startActivityした際にActivityを再生成するか使いまわしするかの設定を指定することができます。
参考:Android launchMode の違い

  • Standard(デフォルト)
  • SingleTop
  • SingleTask
  • SingleInstance

このうち、使うのはStandardとSingleTopです。
むしろSingleTaskやSingleInstanceはよほどの理由がない限り使いません。
(理由は自アプリケーションのタスク外にActivityを保持し、再利用時の挙動が複雑なためです(というかほぼ制御不能に近い)。このため、公式でも非推奨となっています。)

StandardとSingleTopの挙動の違いは下記が参考になります。
SingleTopにしておけば、再度startActivityされた際に生成(onCreate)ではなくonNewIntentの方が呼ばれます、Activity Stack内にも1つです。
参考:launchModeをsingleTopにしておくと何が起こるか

SingleTopは主にルートActivityを使いまわす場合に指定するとよいでしょう。(NavigationViewからのルートActivity呼び出し、ディープリンクなどでのルートActivity呼び出しなどに使えます)

launchMode属性記述例:

<activity            
android:launchMode="singleTop"
android:name="(パッケージ名).HogeActivity"/>

ActivityとFragmentのライフサイクル

ActivityとFragmentはそれぞれライフサイクルを持ちます。
FragmentはActivityに付随しているため、Activity生成後にライフサイクルが開始し、
Actvity破棄時にFragmentのライフサイクルも終了します。
特によく使うコールバックに関してまとめておきます。

Activityのライフサイクル

startActivityもしくはstartActivityForResultされたときにActivityのライフサイクルが開始します。(非同期呼び出し)

1fad3f2c-c7d9-2286-86f8-f51b36dff7b0.jpeg

Activity開始系コールバック

  • onCreate:Activityが生成された(このタイミングでレイアウトファイルをセットする、またonSaveInstanceStateに保存していたパラメータを再取得する)
  • onNewIntent:launchModeがSingleTopの場合に呼ばれる、Activityが再利用された
  • onStart:Activityが表示された(ユーザーからの操作には反応しない)
  • onResume:Activityがユーザーからの操作受付を開始した

Activity終了系コールバック

  • onPause:Activityがユーザーからの操作受付を停止した(この時点では表示はされている)
  • onSavedInstanceState:Activity破棄前にActivityの内部データを保存する
  • onStop:Activityが別Activityによって全部隠された。onPauseの後にアプリが見えなくなった場合に呼ばれる(ホーム画面に戻ったり、他のアプリ(全画面を使用)のアプリを立ち上げた時に実行される)
  • onDestroy:Activityが破棄された(メモリ不足の場合にシステムから破棄される場合も含む)

Fragmentのライフサイクル

FragmentManagerのFragmentTransactionにattach後、commitされたときにFragmentのライフサイクルは開始します。(非同期呼び出し)
FragmentTransactionのadd、replaceなどのメソッドはattachの操作を含みます。

59faad6e-4cc3-6209-8ff2-1d070c316da3.png

Fragment開始系コールバック

  • onAttach:FragmentManagerにFragmentがアタッチしたとき(どのFragmentを表示するか指定した時)に呼ばれる
  • onCreate:Fragmentが生成された
  • onViewCreated:Fragmentのビューが生成された(このタイミングでレイアウトファイルをセットする)
  • onStart:Fragmentが表示された(ユーザーからの操作には反応しない)
  • onResume:Fragmentがユーザーからの操作受付を開始した

Fragment終了系コールバック

  • onPause:Fragmentが非表示になった
  • onStop:onPauseの後にアプリが見えなくなった場合に呼ばれる(ホーム画面に戻ったり、他のアプリ(全画面を使用)のアプリを立ち上げた時に実行される)
  • onDetach:FragmentManagerからFragmentがデタッチしたとき(FragmentManagerから対象のFragmentが外された時)に呼ばれる

Viewのライフサイクル

ActivityのsetContentViewもしくはFragmentのinflateした時にViewのライフサイクルが開始します。

android.png

  • onMeasure:View自身の幅高さを確定させる
  • onLayout:ViewGroupの場合、子Viewの位置を決める
  • onDraw:Viewの描画を行う

参考:onMeasureとonLayoutについて理解する

ActivityやFragmentからViewTreeのレイアウト生成イベントを捕捉するためにはViewTreeObserverを利用します。

参考:【Android】Viewの幅と高さを正確に取得する

カスタムViewを作る

独自Viewを作成する際は下記参考になります。

参考:Androidで独自Viewを作るときの4つのTips +2

ちなみに最近だとAndroid Databindingを使ってViewパラメータのセッターを作成すれば、
attrs.xmlに書かずとも楽にlayoutファイル経由でパラメータを渡せたりします。

参考:Android DataBinding 〜カスタムビューで使う〜

独自のViewGroupを作成するには下記が参考になります。

参考:Androidの独自レイアウトを作成する

ActivityとFragmentのライフサイクルの関係

参考:ActivityとFragmentのライフサイクルと罠

Activity内のFragmentのライフサイクル

  • 起動処理は Activity -> Fragment の順
  • 終了処理は Fragment -> Activity の順

FragmentActivity内のFragmentのライフサイクル

  • 起動処理も終了処理も Activity -> Fragment の順

※ActivityとFragmentActivityを混ぜるな危険

というかFragmentActivityを継承したAppCompatActivityを使いましょう(大抵の人は使っていると思いますが)
サポートライブラリのAppCompatActivityとFragmentを使ったほうが各Android OSバージョンで互換性が保たれています。

  • AppCompatActivity(android.support.v7.app.AppCompatActivity)
  • Fragment(android.support.v4.app.Fragment)

Activityの画面遷移

startActivityもしくは呼び出しActivityの結果を受け取りたい場合は、ActivityもしくはFragmentにてstartActivityForResultメソッドを使います。
結果を受け取りたい場合は呼び出し元のActivityでonActivityResultの実装が必要です。
(onActivityResultはバックボタンで呼び出し先のActivityを終了した際も呼び出し元で呼ばれます)

参考:startActivityForResultのrequestCodeを理解する

呼び出し側実装例:
簡易的にFirstActivityからSecondActivityを呼び出す例を記述します。

FirstActivity.java
public class FirstActivity extends AppCompatActivity {

    private static final int REQUEST_CODE = 100; // リクエストコード(呼び出しActivity識別用)

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        launchSecondActivity();
    }


    // SecondActivityを起動するメソッド
    public void launchSecondActivity(){
        Intent intent = new Intent(this,SecondActivity.class);
        intent.putExtra("ARG","パラメータ"); // 引数渡しする場合はIntentクラスのputExtraメソッド経由で渡す
        startActivityForResult(intent,REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data){
        // 注意:superメソッドは呼ぶようにする
        // Activity側のonActivityResultで呼ばないとFragmentのonActivityResultが呼ばれない
        super.onActivityResult(requestCode,resultCode,data);

        switch(requestCode){
            case(REQUEST_CODE):
                // 呼び出し先のActivityから結果を受け取る
                if(resultCode == RESULT_OK){
                    String result = data.getStringExtra("RESULT");
                    Log.d("ログ",result);
                }
                break;
            default:
                break;
        }
    }

}

呼び出し先のActivityでは呼び出し元のActivityに結果を返す処理を記述します。

SecondActivity.java
public class SecondActivity extends AppCompatActivity{


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        Intent intent = getIntent();
        String arg = intent.getStringExtra("ARG"); // 呼び出し元から引数を受け取る
        sendResult(arg);
    }

    public void sendResult(String result){
        Intent intent = new Intent();
        intent.putExtra("RESULT",result);
        setResult(RESULT_OK, intent);
        finish();
    }

}

FirstActivityとSecondActivityはAndroidManifest.xmlにあらかじめ追加する必要があります。

AndroidManifest.xml
<application>
        <activity android:name=".activity.FirstActivity"/>
        <activity android:name=".activity.SecondActivity"/>
</application>

注意点

厄介なことにActivityに紐づいているFragment内の子Fragment(Fragment in Fragment)のonActivityResultにはコールバックが来ないようです。
対策として、Fragment内のonActivityResultメソッドにて子FragmentのonActivityResultに処理を伝播させる処理を記述するなどが必要です。

補足

EventBusのstickyイベントを利用すると
呼び出し元以外にも一括で更新データの表示反映することが可能になります。
この方式のメリットは非表示状態やタブ等をまたいだActivityやFragmentに更新通知を行うことが可能になります。
デメリットはグローバルにイベントをPubSubしてしまうので気を付けて使わないとSubscribe箇所で事故りやすいことです。

参考:【Android】EventBus 3のドキュメントを読んでみた

例えば次のような、更新イベントを作成します。

RefreshEvent.java
public class RefreshEvent {
    private Class target;

    public RefreshEvent(Class target){
        this.target = target;
    }

    public Class getTarget(){
        return this.target;
    }

}

SecondActivity側から更新イベントを対象のActivityもしくはFragmentに送信します(今回の例はFirstActivity)。

SecondActivity.java
   // ここでSharedPreference等にデータ保存処理をする

   // FirstActivityの表示を更新する
   EventBus.getDefault().postSticky(new RefreshEvent(FirstActivity.class));

受信側は次のような処理になります。

FirstActivity.java
    @Override
    protected void onStart() {
        super.onStart();
        EventBus.getDefault().register(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        EventBus.getDefault().unregister(this);
    }

    @Subscribe(sticky = true)
    public void onRefresh(RefreshEvent event){
        if(event.getTarget() == this.getClass()){
            // 表示更新処理

            // 削除し忘れ注意
            EventBus.getDefault().removeStickyEvent(RefreshEvent.class);
        }
    }

Fragmentの画面遷移

Fragmentの画面遷移にはFragmentManagerとFragmentTransitionを使います。
Fragmentに引数を渡すにはsetArgumentsメソッドを使います。

ここではMainActivityからMainFragmentを生成して画面をattachしてみます。(add→commit)

MainActivity.java
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if(savedInstanceState == null){
            FragmentManager fragmentManager = getSupportFragmentManager();
            MainFragment mainFragment = new MainFragment();
            Bundle bundle = new Bundle();
            bundle.putString("ARG","パラメータ");
            mainFragment.setArguments(bundle);
            fragmentManager.beginTransaction()
                    .add(R.id.container,mainFragment)
                    .commit();
            fragmentManager.executePendingTransactions(); // FragmentのTransaction処理の完了同期待ち(必須ではない)
        }
    }

Fragmentの生成(new)は一度のみ(savedInstanceState == null)で大丈夫です。
(onSavedInstanceStateで自動的にFragmentManagerはFragmentのインスタンスを保持&再生するため)
MainFragment側ではgetArgumentsで引数を受け取ります。

MainFragment.java
public class MainFragment extends Fragment {

    String arg;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle bundle = getArguments();
        arg = bundle.getString("ARG"); // パラメータ取得
    }

}

また、FragmentArgsライブラリを使うとsetArgumentsの記述が少し楽になります。
参考:Fragment引数の DI

呼び出し先Fragmentから呼び出し元Fragmentへ結果を返すにはsetTargetFragmentメソッドを使います。

参考:Fragmentで呼び出し元に結果を伝える

Fragment周りはほかにも色々気を付けないといけないところいっぱいあります。
参考:【Android】Fragmentを使うときのコツとか色々
参考:Fragment使用時のIllegalStateException回避

ActivityとFragmentとViewの状態保存

回転させたときやアプリバックグラウンド時のメモリ不足時にはOOM KillerやLow Memory Killerにより、ActivityやFragment(それらに付随しているViewも)すぐに死にます。(onPause→onSavedInstanceState→onStop→onDestroyされる)
そのため、onSavedInstanceStateにてパラメータをBundle(Viewの場合はParcelable)に保存し、onCreateにて再取得し状態を復元する必要があります。

また、BundleやParcelableにあまり大きいデータを保存するのはよくありません。
理由はシステム領域のメモリに一時保存されているのですが、この保存領域の上限を超えてしまうとアプリが異常終了します。
一時的ではない保存データはSharePreferenceやSQliteやRealmなどを利用して永続化する必要があります。

Icepickライブラリを使うとonSavedInstanceStateメソッドの記述やonCreateでの再取得が少し楽になったりします。
参考:ライフサイクル時の変数保存

Activityの状態保存

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

Activityが破棄されている場合にはActivityはonCreateから再生成されます。

Fragmentの状態保存

参考:Android Fragment で setArguments() してるサンプルが多いのはなぜ?

Fragment引数の状態を保持するためにはsetArgumentsにてBundle経由でFragment生成時の値を渡します。コンストラクタやsetter経由でFragmentに値を渡してはいけません。

Fragment引数以外のデータの一時保存にはActivity同様、onSaveInstanceStateとonCreateメソッドにて行います。

Viewの状態保存

参考:Android Fragment は破棄時に保持している View の状態を保存させている
カスタムViewのデータ保持例:fragment上のcustomViewの状態再生成方法

Fragment内の基本のViewであればFragmentに値が保存されてFragment再生成時にViewのパラメータも復元されますが
カスタムViewの状態保存にはカスタムViewのonSavedInstanceStateのParcelableにて状態保存し、onRestoreInstanceStateにて状態を復元します。

実機のデバッグ設定

実機で設定しておくとデバッグに使えます。

タップ位置、タップ履歴を表示する

開発者モードのタップ表示、ポインタの位置をオンにします。
タップ領域がおかしいなとおもったら、この設定にしてみましょう

Screenshot_2017-05-22-11-06-08.png

onSavedInstanceStateのデバッグ

Activityを常に破棄する設定をオンにするとホームボタンを押した際にActivityが常に破棄されるようになります。
onSavedInstanceStateのデバッグに使えます。

Screenshot_2017-05-22-11-06-29.png

アプリ再インストール時にSharedPreferenceデータを復元しない

バックアップ設定の自動復元がオンになっているとSharedPreferenceのデータがアプリ再インストール時に復元されます。
初回起動判定フラグ等をSharedPreferenceに保存しているとフラグ毎復元されるため、
デバッグ時にはオフにして確認します。

Screenshot_2017-05-22-11-07-02.png

その他

Android Studio上でActivityやFragment内で
Ctrl+O(Macはコマンド+O)キーを押すと
@Override系のメソッドが補間できるのでコーディングが楽です。
Alt+Enterと合わせて積極的に使っていきましょう。

teradonburi
気管支喘息を患って死にかけです。 いつ死ぬかわかりません。 成人喘息は誰でもなりえるものだし、 咳喘息から気管支喘息に進行すると慢性的な死の危険があるものです。 自身や周りで数週間咳が続いてる人がいたら気をつけて・・・ Twitterフォローいただけたらフォロバします。 https://twitter.com/teradonburi
meetsmore
プロを探せる見積りプラットフォーム「ミツモア」の開発・運営
https://meetsmore.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away