はじめに
個人的な経験から、Fragmentを使うときに気にしたり気づいたことをまとめました。
Fragmentの初期設定値とか、生成時に値を渡す時はsetArguments
Fragmentのコンストラクタはpublicで無修正なやつじゃないと怒られるので、コンストラクタをカスタマイズしてnewする時に値を渡すことが出来ない。
また、getInstance等を作って値を渡すことを考えるかもしれないが、Fragmentが破棄されて再生成されるときに動的フィールドは全て初期化される。このとき最初からライフサイクルが走り直すが、getInstanceを通らないので初期値を受け取れず、本来の動作を行えなくなる。
なのでFragmentセット時に値を渡して、それで状態を切り替えたりしたいときは setArguments() か onSaveInstanceState() を使おう。
使い方は以下のとおり。
setArguments()
// データを渡す為のBundleを生成し、渡すデータを内包させる
Bundle bundle = new Bundle();
bundle.putString("URL", "http://hogehoge.com");
// Fragmentを生成し、setArgumentsで先ほどのbundleをセットする
HogeFragment fragment = new HogeFragment();
fragment.setArguments(bundle);
// FragmentをFragmentManagerにセットする
getFragmentManager().beginTransaction()
.add(R.id.container, fragment, HogeFragment.TAG)
.commit();
Fragment側で受け取るときは
Bundle bundle = getArguments();
String url = bundle.getString("URL");
ちなみにsetArgumentsした情報は、Fragmentが再生成されてライフサイクルが走り直しになってもデータはセットされたままになっているので、再セットしなくても利用し続けることができる。
onSaveInstanceState()
こちらもやり方は簡単。
onSaveInstanceState()を呼び出す、あるいは呼び出されたタイミングで、引数のBundleに保持したい値をセットする。
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putString("KEY", "value");
super.onSaveInstanceState(outState);
}
こうすることで、再生成された時に走るonCreateViewやonActivityCreatedで、引数にある Bundle savedInstanceState に対し値がセットされている。
初期値設定としては手順もあって、再生成時とそうでない時の分岐などが面倒なので、画面に対する初期値設定はsetArguments()で、Fragment内で発生したデータやViewへのinput状況を保持する場合にはonSaveInstanceState()を使う、といった使い分けが必要かもしれない。
Fragmentの切り替えはreplace?show/hide?
おさらい
-
Add/Remove
【Add】
FragmentManagerにFragmentを追加する。
追加されたらonAttachからライフサイクルが始まる。
既にAddされているインスタンスの場合は何も起きない。
【Remove】
FragmentManagerからFragmentを外す。 -
Replace
セットされているFragmentを全てRemoveしてから、指定のFragmentをAddする。
既に追加されているインスタンスでReplaceすると、変な動作をする?
(表示されなかったりする?)
コメント投稿からの情報提供(@hkusuさん)によると「何もイベントが起きない」という挙動のようです。 -
Show/Hide
セットされている(Add済みの)Fragmentの表示/非表示を切り替える。
このとき、ライフサイクルは変化しない。
Fragmentの切替方法は、アプリの階層構造をもとに考えるべき。
例えば同一階層の画面切替(NavigationDrawerによるページ切替等)がトップ画面なら、端末のバックボタンを押せばアプリが終了する筈なのでReplaceで切り替えれば充分。
例えばTabで画面を切り替えるけど、各画面の状態は維持しておきたい場合などはViewPagerと連動させたりShow/Hideで切り替えることを選ぶと良さそう。
addToBackStackによる画面バックを実装する場合は 遷移することで画面階層が下がるとき。
他にもBackStackの注意事項として、BackStackをClearしたい時に
getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
こんな感じの処理を書くことになると思うが、このとき全てのBackStackの分をpopしてることになるので、Fragment遷移を溜め込んだらそれぞれがaddされる時のライフサイクルが動いてしまう。onAttach〜onResumeまでの処理に注意しよう。
画面遷移とかアプリ全体にかかわるイベントはActivityに丸投げ
getActivityは何故かnullだったりして嫌がらせしてくる。かといってActivityの状態を考えず
HogeActivity activity = HogeActivity.getInstance();
こういうことしちゃうのもかなC。基本的にオブジェクトの寿命よりライフサイクルを意識してActivityとFragmentは使用すべきと思う。
自分がセットされたActivityに対して処理を渡したいときは、onAttachの引数にあるactivityを保持しよう。
private HogeActivity activity = null;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (activity instanceof HogeActivity) {
this.activity = activity;
}
}
FragmentからActivityの処理を呼び出したい時に、この受け取ったactivityを直接操作してもプログラム的に「絶対このActivityにしかセットしません!!!!!」というのなら問題ない。
でもここでいうHogeActivityにどんなメソッドが用意されてるかとか知る由もない時もあるし、なんかActivityとFragmentくっつき過ぎじゃない?自分ら付き合ってんの?という感じもするので、もうちょっとつながりを薄めたい。
ここでよくやる方法が、Fragment側で独自のイベントリスナを作り、Activityに実装させるパターン。 こちらもonAttachで渡されるactivity引数を用いるが、activityのpublicメソッドを直接呼び出すようなことをしなくて良くなる。
public class HogeActivity extends Activity implements
HogeFragment.HogeFragmentListener {
@Override
public void onHogeFragmentEvent1() {
// Fragmentからイベント通知がきた時の処理
}
@Override
public void onHogeFragmentEvent2() {
// Fragmentからイベント通知がきた時の処理
}
@Override
public void onHogeFragmentEvent3() {
// Fragmentからイベント通知がきた時の処理
}
}
private HogeFragmentListener listener = null;
public interface HogeFragmentListener {
void onHogeFragmentEvent1();
void onHogeFragmentEvent2();
void onHogeFragmentEvent3();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// 実装されてなかったらException吐かせて実装者に伝える
if (!(activity instanceof HogeFragmentListener)) {
throw new UnsupportedOperationException(
"Listener is not Implementation.");
} else {
// ここでActivityのインスタンスではなくActivityに実装されたイベントリスナを取得
listener = (HogeFragmentListener) activity;
}
}
// クリックイベントで画面遷移するときとか
@Override
public void onClick(View v) {
if (listener != null) {
// Activityにイベント通知
listener.onHogeFragmentEvent1();
}
}
こんな感じで実装すれば、処理の単棟がわかりやすくなるし、Acitivity側でどんな処理を呼び出すかはActivityに任せることが出来る。
しかしこうなるとActivity側のコード量が増える可能性も出てくる。
だいたいFragmentというのは1つや2つでは済まないパターンが多いので、イベント量が増えるに連れてマシュマロ系Activityに進化していってしまう。
そこでイベント処理をイベント処理クラスとして分割してしまおうという方法がある。
以下の記事でも書いてあるが、こちらでも概要を説明する。
【Android】EnumでFragment→Activityのイベントを実装
基本的な作りは先ほどのイベントリスナパターンだが、通知周りを少し変える。
まずはFragmentからActivityに渡すEnumクラスを作成する。
public enum HogeEvent {
EVENT1 {
@Override
public void apply(Activity activity) {
// Fragment→ActivityのActivity側の処理とか
}
},
EVENT2 {
@Override
public void apply(Activity activity) {
// Fragment→ActivityのActivity側の処理とか
}
},
EVENT3 {
@Override
public void apply(Activity activity) {
// Fragment→ActivityのActivity側の処理とか
}
};
abstract public void apply(Activity activity);
}
次にFragmentListenerを修正する。(onAttachの処理は同じ)
public interface HogeFragmentListener {
void onHogeFragmentEvent(HogeEvent event);
}
// クリックイベントで画面遷移するときとか
@Override
public void onClick(View v) {
if (listener != null) {
// Activityにイベント通知
listener.onHogeFragmentEvent(HogeEvent.EVENT1);
}
}
Activity側は最低限の記述で、全てのイベントに対応できる。
@Override
public void HogeFragmentEvent(HogeEvent event) {
event.apply(this);
}
これでActivityはFragment管理とイベントの実行、Fragmentは画面表示と画面イベントの通知、EventはFragment→Activityのイベントの処理実態という3分割ができた。
ActivityからFragmentを操作しよう
ActivityからFragmentを操作するときは、頻繁でなければできるだけ安全にfindFragmentByTag()で取得したFragmentのメソッドを叩きたい。
if (getFragmentManager().findFragmentByTag(HogeFragment.TAG) != null &&
getFragmentManager().findFragmentByTag(HogeFragment.TAG) instanceof HogeFragment) {
((HogeFragment) getFragmentManager().findFragmentByTag(HogeFragment.TAG)).hoge();
}
ActivityのフィールドにセットしたFragmentのインスタンスを持っていると、再生成等でnullにされる可能性があるので、nullならfindFragmentByTag()で再取得するような処理を実装したほうがよさそう。
FragmentってView?Controller?ActivityはController???ワァーーー!!!
よくある議論、MVCで考えるときにActivityはControllerなのか、Viewなのか。
はてはFragmentがViewでActivityがControllerなのか。
xmlがViewでFragmentもControllerとかそんなのもある。
ごく小規模の人数で開発するときのモバイルアプリ開発に関して言えば、対して重要ではない気がする。
もちろん会社員として携わる場合など、後に引き継ぐ可能性があれば可読性やフレームワーク的な一貫性を考えて然るべきだけど、MVCや拡張MVC、MVVMなどにあえて当てはめて考える必要はないと思う。(ある程度大きな規模で、かつ綿密な設計とそれに伴うドキュメントが整備されるのであればかなり有用なものになるとも思っているけど)
個人的な分類としては
- Model
- Event(Enum)
- Activity
- FragmentListener
- Fragment
主にこの5つで考えている。(それぞれが他でいう何かはこの際考えない)
Modelはいわゆるビジネスロジック、ライフサイクルに影響されない独立した内部処理。
Activityはアプリ全体(あるいはアプリの大要素の一つ)の処理として、ライフサイクルに応じてModelの呼び出しやFragmentからの通知を受け取ってEventを実行したりする。
あくまでもサービス一つ(広義のアプリというのか)の状態を管理し、Fragmentの管理者という役割を中心に持たせる。
(マシュマロ系Activityの回避も考慮して)
FragmentはViewの操作を受け取ったり、あるいはライフサイクルに応じて、FragmentListenerを介してActivityにイベント通知を行う。処理は完全に移譲してEventの種類だけを渡す。
実際の画面要素であり、表示中のView状態を管理して内部に伝える役割を持たせる。
EventはFragmentがActivityに移譲する処理の実態を定数化したもの。
(個人的には)Enumで作成し、Activityで実行するときのコード量を減らしたい。
これによりActivityとFragmentがクラス的に独立し、Fragment(画面)側でViewと発生イベントの組み換えがあってもActivityの処理変更を最低限で済ませることができる。
Activity、Fragment、Event全般の汎用性?が増し、モバイルアプリでありがちな「やっぱりこっちがいいな」の対応力が向上するというオマケもついてくる(気がする)
これでも正直、何も言わずにこれで実装されてたら引き継いだ人もポカーンなので、やはり面倒くさがらず時間を見つけてドキュメントを作っておくべきだろう。
スプラッシュ画面はFragmentで作ろう
スプラッシュ画面とかホンマやめよう。
でも欲しいって言われるよね、開発でも初期化処理とかするのにちょうどいいじゃんとか。
そんな色々初期化処理が必要なサービスってスマホアプリ的にはどうなんでしょうね。
ログイン処理とかならわかるけどね。初期化処理するにしても、アプリが立ち上がってからそういう動きさせればいいじゃんっていうのもあるよね。でもしょうがないね。
作るならActivityでスプラッシュ画面を実装するのは避けよう。
SplashActivity→MainActivityな遷移を作ることになると思うけど、例えばMainActivity開いたままアプリアイコンからまた起動しちゃうと、(素直に組んでると)MainActivityが2個できちゃうね。
launchMode変えりゃ良いじゃんって話だけど、基本的にはstandardで大丈夫なように作るのが理想。
何よりlaunchModeは(ぼくにとって)結構難しいので、できるだけ変更したくない。
Push通知機能とかで、どこからでもアプリが立ち上がっちゃうときはlaunchModeでの制御が必要になるから、そういう時はちゃんと調べて勉強してから使っていこう。必要な時は使うべき。
しかしActivity→Activityの遷移はFragment切替より遅いので、やはりユーザビリティ的にはFragmentで実装して少しでも快適な動作をしたい。
public class HogeActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hoge);
// SplashFragmentセット
getFragmentManager().beginTransaction()
.replace(R.id.container, new SplashFragment, SplashFragment.TAG)
.commit();
}
}
基本的にはActivityの挙動に合わせて、onCreateでセットしよう。SplashFragmentの中で初期化処理を実装しても良いかもしれない。
画面回転に対応する時は、savedInstanceStateを使ってうまく分岐させるようにしよう。
Fragmentのテンプレートとか
Android Studioを使って開発しているなら、ファイルテンプレート活用するとクラス作成が楽になる。
場所は(Macなら)「Preferences > Editor > File and Code Templates」
左上の「+」を押して、自分のテンプレートを作ろう。
以下はテンプレートの例
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
#parse("File Header.java")
public class ${NAME} extends Fragment {
/* ---------------------------------------------------------------------- */
/* Field */
/* ---------------------------------------------------------------------- */
public static final String TAG = ${NAME}.class.getSimpleName();
private Activity activity = null;
private View view = null;
private ${NAME}Listener listener = null;
/* ---------------------------------------------------------------------- */
/* Listener */
/* ---------------------------------------------------------------------- */
public interface ${NAME}Listener {
void onHogeEvent();
}
/* ---------------------------------------------------------------------- */
/* Lifecycle */
/* ---------------------------------------------------------------------- */
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
Log.d(TAG, "onAttach");
if (!(activity instanceof ${NAME}Listener)) {
throw new UnsupportedOperationException(
TAG + ":" + "Listener is not Implementation.");
} else {
listener = (${NAME}Listener) activity;
}
this.activity = activity;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Log.d(TAG, "onCreateView");
view = inflater.inflate(R.layout.hoge, container, false);
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Log.d(TAG, "onViewCreated");
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.d(TAG, "onActivityCreated");
}
@Override
public void onStart() {
super.onStart();
Log.d(TAG, "onStart");
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume");
}
@Override
public void onStop() {
super.onStop();
Log.d(TAG, "onStop");
}
@Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
}
}
もしよければ参考にしてあげて下さい。
他にも色々まとめ中。
ここ間違っとるぞカスってところがあったら教えて下さい。