あるActivityから別のActivityを呼び出すときや、Fragmentを新規に生成する時、ExtraやArgumentと呼ばれる、Bundle型の初期値を渡すことができます。
このとき、「渡す側」と「渡される側」に別れるわけですが、どちらがどこまで相手のことを知っていなければならないか?という関心ごとは、分業していく上で重要なファクターになります。
この辺のやり方についてデファクトスタンダード的なものが無いように思えたので、今回は私が現在良いと思っているスタイルを紹介したいと思います。
(5/19追記:Twitterで色々と御意見をお聞きしまして、その後また自分の中でのベストプラクティスが変わってきています。記事末尾の追記も是非お読みください)
Fragmentについても読み替え可能な部分がほとんどなので、Activityについて解説していきます。
よくあるやつ(初級編)
初心者向けのサイトや本に書いてありそうな使い方は、下記のようなものです。
public class PeopleActivity extends Activity {
private void showPerson(String personId) {
Intent intent = new Intent(this, PersonActivity.class);
intent.putExtra("person_id", personId);
startActivity(intent);
}
}
public class PersonActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String personId = getIntent().getStringExtra("person_id");
// initialize with personId
}
}
チュートリアルにあるやつそのまんまといった感じですね。
各Activityクラスが関心を持っていないといけない条件に着目してみます。
-
"person_id"
という文字列を両者が知っていなければならない - 渡される側が初期化処理をするために必要なExtraは
personId
のみなのかどうかを、渡す側が知っていなければならない
両方のクラスを1人の開発者が作る場合には、あまり意識していなくても成立する条件ばかりですが、バグの埋め込みやすさという点に着目すると、少し難があります。
試しに、それぞれの条件を崩してみましょう。
not("person_id"
という文字列を両者が知っていなければならない)
public class PeopleActivity extends Activity {
private void showPerson(String personId) {
Intent intent = new Intent(this, PersonActivity.class);
intent.putExtra("personId", personId); // => Oops!
startActivity(intent);
}
}
public class PersonActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String personId = getIntent().getStringExtra("person_id");
// initialize with personId
}
}
何かを血迷ったアホが独自色溢れる命名規則でキー名を付け始めた途端に破綻します。
not(渡される側が初期化処理をするために必要なExtraは personId
のみなのかどうかを、渡す側が知っていなければならない)
public class PeopleActivity extends Activity {
private void showPerson(String personId) {
Intent intent = new Intent(this, PersonActivity.class);
intent.putExtra("person_id", personId);
// modeId is not set
startActivity(intent);
}
}
public class PersonActivity extends Activity {
public static final int MODE_DEFAULT = 100;
public static final int MODE_PRIVACY = 200;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
String personId = extras.getString("person_id");
int modeId = extras.getInt("mode_id"); // => 0
// initialize with personId and modeId
// 0 is unknown modeId => Oops!
}
}
必要なパラメータを渡す側が知らなかった場合にも破綻します。複数箇所から呼び出される画面を改修した場合に、渡される側は改修したけど、渡す側の改修に抜け漏れがあった、というパターンで問題が起きそうです。
実用的によくあるやつ
ここまで紹介してきたものは、「サンプルをコピペしただけじゃ使い物になんねーよ?」という話でしかないので、次はちゃんとしたAndroidプログラマー諸兄がやっていそうな素直な書き方にしてみます。
public class PeopleActivity extends Activity {
private void showPerson(String personId) {
Intent intent = new Intent(this, PersonActivity.class);
intent.putExtra(PersonActivity.EXTRA_PERSON_ID, personId);
// intent.putExtra(PersonActivity.EXTRA_MODE_ID, MODE_PRIVACY);
// ^^^if needed^^^
startActivity(intent);
}
}
public class PersonActivity extends Activity {
public static final String EXTRA_PERSON_ID = "person_id";
public static final String EXTRA_MODE_ID = "mode_id";
public static final int MODE_DEFAULT = 100;
public static final int MODE_PRIVACY = 200;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
String personId = extras.getString(EXTRA_PERSON_ID);
int modeId = extras.getInt(EXTRA_MODE_ID, MODE_DEFAULT);
if (personId == null) {
throw new IllegalArgumentException("extra 'person_id' must not be null");
}
// initialize with personId and modeId
}
}
渡される側が全力で自衛したパターンです。大枠としては最初のコードにあった問題が解決されています。
「"person_id"
という文字列を両者が知っていなければならない」対策
EXTRA_PERSON_ID
という定数を設けることで解決しました。少なくとも、文字列の内容が変わっても、機能上の問題は起こらなくなっています。(別のExtraと名前が被らない限りは、ですが)
その代わり、今度は「渡す側が EXTRA_PERSON_ID
という定数の存在を知っていなければならない」という条件が発生しました。
「渡される側が初期化処理をするために必要なExtraは personId
のみなのかどうかを、渡す側が知っていなければならない」対策
2種類の改善を行いました。
- 絶対に必要なパラメータが渡されていない場合、早期に発覚させてExceptionを吐かせて落とす
- 「そういう使い方するActivityじゃねえから!!」ということを呼び出し元の開発者に早めに気付かせる
- 渡されていないパラメータにデフォルト値を用意できる場合、デフォルト値を採用する
- 特殊なことをしようとしていない分には知らなくても何とかなるように
ここまで来るとメソッドで受け取った引数をどう扱うかの話と似たようなところになってしまっていますが、まあ本質的には似たようなものだと思います。
最初に比べれば開発者にとって親切なコードになりましたが、「必要なパラメータは何なのか」という点については、少し不満が残ります。一度Exceptionに怒られるまではどのパラメータが必要なのか分からないのは、ザクザクコードを書いていきたい段階の時にはちょっと嫌ですね。
だからといって、これから呼び出すActivityのコードを読みに行くのも、やらずに済むならやりたくないですね。(ここまで来るとただの怠惰)
手っ取り早い解決策としては、javadocコメントを書くことです。
/**
* required extras: {@link #EXTRA_PERSON_ID} <br>
* optional extras: {@link #EXTRA_MODE_ID}
*/
public class PersonActivity extends Activity {
これで、Android StudioなどのIDEからjavadocを確認できるようになりました。
つまり、Intentのインスタンスを作ったら、呼び出し先Activityのjavadocをいちいち見に行って、パラメータの要不要を確認する文化をチーム内で作れれば・・・
それはそれでめんどくさいね!!?
ナウなヤングにバカウケのやつ
メソッドを呼び出すような気軽さと堅牢さで、Activityを呼び出せないものかという観点では、下記のようなやり方が有効ではないかと感じています。
public class PeopleActivity extends Activity {
private void showPerson(String personId) {
PersonActivity.startActivity(this, personId);
// or
// PersonActivity.startActivity(this, personId, PersonActivity.MODE_PRIVACY);
}
}
public class PersonActivity extends Activity {
private static final String EXTRA_PERSON_ID = "person_id";
private static final String EXTRA_MODE_ID = "mode_id";
public static final int MODE_DEFAULT = 100;
public static final int MODE_PRIVACY = 200;
@IntDef({MODE_DEFAULT, MODE_PRIVACY})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayMode{}
/**
* Start PersonActivity<br>
* if you need to change mode, call {@link #startActivity(Context, String, int)}
*
* @param context calls from
* @param personId person's id
*/
public static void startActivity(Context context, String personId) {
startActivity(context, personId, MODE_DEFAULT);
}
/**
* Start activity with mode id
* @param context calls from
* @param personId person's id
* @param modeId optional. {@link #MODE_PRIVACY} enables ****
*/
public static void startActivity(Context context, String personId, @DisplayMode int modeId) {
if (personId == null) {
throw new IllegalArgumentException("extra 'person_id' must not be null");
}
Intent intent = new Intent(context, PersonActivity.class);
intent.putExtra(EXTRA_PERSON_ID, personId);
intent.putExtra(EXTRA_MODE_ID, modeId);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
String personId = extras.getString(EXTRA_PERSON_ID); // => not null
int modeId = extras.getInt(EXTRA_MODE_ID); // => not 0
// initialize with personId and modeId
}
}
Context#startActivity
の実行まで含めて、全ての責務を渡される側に移しました。
-
EXTRA_PERSON_ID
などの名前を知らなくてもExtraを指定できる - 渡される側の都合で、渡す側が渡してくるExtraの型を強要することができる
- 渡される側の都合で、RequiredなExtraとOptionalなExtraを渡す側に強要することができる
- 渡される側は自分が必要としている値を自分のクラス内に書けるので、値が本当に渡されてくるかどうかを制御しやすくなる
- 渡す側が書きやすい
といった具合に、前項で残っていた課題も解決されています。
Android Studioで楽しくstartActivity
実はこのやり方、 全力でAndroid Studioに媚びています。
まずはメソッドを書き始めてみましょう
この時点で、「どうやらPersonActivityはmodeIdというパラメータを受け取ることもできるらしい」ということに自然な流れで気付くことができます。
javadocでも第3引数の存在に言及しておくと、さらに親切ですね。
しかしこの時点では、第3引数にどんな数値を入れたらいいのか分かりませんね?
まあ適当に 1000
とか入れちゃってみましょう。
おや、怒られましたね。マウスオーバーしてみると・・・
「Hey,ブラザー。そこにブチ込めるものは限られてるんだぜ。俺の自慢のdickだってソイツには勝てねえんだHAHAHA!!!」といった気軽さで、入れられる値の制限内容を教えてくれます。
これには、Support Library 19.1から導入されたアノテーションの1つ、 @IntDef
を使っています。
@IntDef
アノテーションによって作られた @DisplayMode
アノテーションを付与することで、int型の引数に渡せる内容を制限できているのです。
@IntDef({MODE_DEFAULT, MODE_PRIVACY})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayMode{}
public static void startActivity(Context context, String personId, @DisplayMode int modeId) {
こういったアノテーションを用意しておくと、入力制限はもちろん、補完にも効いてきます。
Fragmentの場合
Fragmentになってもやることはあまり変わりませんが、例を挙げます。
private void initPersonFragment(String personId) {
Fragment fragment = PersonFragment.newInstance(personId);
// or
// Fragment fragment = PersonFragment.newInstance(personId, PersonFragment.MODE_PRIVACY);
getSupportFragmentManager().beginTransaction()
.add(R.id.fl_fragment, fragment)
.commit();
}
public class PersonFragment extends Fragment {
private static final String KEY_PERSON_ID = "person_id";
private static final String KEY_MODE_ID = "mode_id";
public static final int MODE_DEFAULT = 100;
public static final int MODE_PRIVACY = 200;
@IntDef({MODE_DEFAULT, MODE_PRIVACY})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayMode{}
/**
* Create PersonFragment instance<br>
* if you need to change mode, call {@link #newInstance(String, int)} }
*
* @param personId person's id
* @return PersonFragment instance
*/
public static PersonFragment newInstance(String personId) {
return newInstance(personId, MODE_DEFAULT);
}
/**
* Create PersonFragment instance
*
* @param personId person's id
* @param modeId optional. {@link #MODE_PRIVACY} enables ****
* @return PersonFragment instance
*/
public static PersonFragment newInstance(String personId, @DisplayMode int modeId) {
PersonFragment fragment = new PersonFragment();
Bundle args = new Bundle();
args.putString(KEY_PERSON_ID, personId);
args.putInt(KEY_MODE_ID, modeId);
fragment.setArguments(args);
return fragment;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Bundle args = getArguments();
String personId = args.getString(KEY_PERSON_ID); // => not null
int modeId = args.getInt(KEY_MODE_ID); // => not 0
// initialize with personId and modeId
}
}
空コンストラクタの強制
Fragmentだけに特筆すべきメリットとして、空コンストラクタを使ってインスタンスを作ることを利用者に強制できるという点があります。
Fragmentに初期値を与えるときにはコンストラクタ引数でもsetterでもなく、Argumentsを使おうというのが鉄則なので、これを強制できるのは非常に価値があります。
このスタイルのデメリット
デメリットとしては、細かい設定がしづらくなるという点があります。Intentにflagを付けたりactionを付けたりしたいときですね。
とはいえ、そういうパターンは比較的少ないと思うので、そういうときには生でIntentを書いてもいいと思います。
※ 後述のcreateIntent派になると、このデメリットが解決されます。
余談: staticおじさん対策
staticメソッドによって堅牢な形でstartActivity/newInstanceを表現するのが本記事のキモになるわけですが、「staticを使うとstartActivityがセキュアになる」みたいな宇宙人的な発想をするstaticおじさんが勘違いをしないためにハッキリと言っておきます。
private static String sPersonId;
public static void startActivity(Context context, String personId) {
sPersonId = personId; // F*ck!!!!!
Intent intent = new Intent(context, PersonActivity.class);
context.startActivity(intent);
}
こういうコードを書くやつは!!!!!!!! 滅びろ!!!!!!!!!!!!!
柔らかい言い方による再掲
Android Javaのstatic変数の生存期間はサーバーやデスクトップのそれに比べるとかなり不安定なはずなので、使うとあるとき突然に値がなくなったりします。使っちゃダメだよ☆
まとめ
今回紹介した HogeActivity.startActivity(context, params)
や HogeFragment.newInstance(params)
のような形をチーム内の文化として共有したり、規約として定めたりすることで、より堅牢で使いやすいActivity/Fragmentを生み出すことができるのではないでしょうか。
それでは皆様、快適なAndroidライフを。
宣伝
ウォーターセル株式会社では、地球人口100億の時代に見合う食料生産のための農業革命を一緒に引っ張っていってくれるAndroid/iOSエンジニアを探しています。
弊社事業に興味のある方は、是非@Nkznに空リプを送るなどしてご連絡ください。
追記
ActivityのExtraやFragmentのArgumentを設定する責務は、呼び出される側に持たせたほうがいいんじゃねーのという提案 by @Nkzn http://t.co/9z0AQFHgmI これ最近良いなと思ってる。getCallingIntentの方が良いかな?
— @wada811 (@wada811) 2015, 5月 16
startActivity(HogeActivity.getCallingIntent(params))
みたいに呼び出せるやりかたですね。Intentを弄れる余地を残したい場合にはgetCallingIntentのほうが良さそうですねー。
追記2
まったくそのとおりで、うちでは `FooActivity.createIntent()` / `FooFragment.newInstance()` で統一してる / “Android - ActivityのExtraやFragm…” http://t.co/yyX4U8B2ch
— Fuji, Goro (@__gfx__) 2015, 5月 17
私もメソッド名は createIntent(), newInstance() にしてるな
— Yuki Anzai (@yanzm) 2015, 5月 17
>> Android - ActivityのExtraやFragmentのArgumentを設定する責務は、呼び出される側に持たせたほうがいいんじゃねーのという提案 http://t.co/2gGn0nGBjA 支持。createIntent, newInstance 派です
— あほむさん (@ahomu) 2015, 5月 18
おっおっ( ^ω^)
createIntent派がのほうが強い感じがしてきました