Android

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

More than 1 year has 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と合わせて積極的に使っていきましょう。