Edited at

ActivityのExtraやFragmentのArgumentを設定する責務は、呼び出される側に持たせたほうがいいんじゃねーのという提案

More than 3 years have passed since last update.

ある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種類の改善を行いました。


  1. 絶対に必要なパラメータが渡されていない場合、早期に発覚させてExceptionを吐かせて落とす


    • 「そういう使い方するActivityじゃねえから!!」ということを呼び出し元の開発者に早めに気付かせる



  2. 渡されていないパラメータにデフォルト値を用意できる場合、デフォルト値を採用する


    • 特殊なことをしようとしていない分には知らなくても何とかなるように



ここまで来るとメソッドで受け取った引数をどう扱うかの話と似たようなところになってしまっていますが、まあ本質的には似たようなものだと思います。

最初に比べれば開発者にとって親切なコードになりましたが、「必要なパラメータは何なのか」という点については、少し不満が残ります。一度Exceptionに怒られるまではどのパラメータが必要なのか分からないのは、ザクザクコードを書いていきたい段階の時にはちょっと嫌ですね。

だからといって、これから呼び出すActivityのコードを読みに行くのも、やらずに済むならやりたくないですね。(ここまで来るとただの怠惰)

手っ取り早い解決策としては、javadocコメントを書くことです。


javadocを書く

/**

* required extras: {@link #EXTRA_PERSON_ID} <br>
* optional extras: {@link #EXTRA_MODE_ID}
*/

public class PersonActivity extends Activity {

これで、Android StudioなどのIDEからjavadocを確認できるようになりました。

スクリーンショット 2015-05-16 17.19.14.png

つまり、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に媚びています。

まずはメソッドを書き始めてみましょう

スクリーンショット 2015-05-16 18.08.35.png

この時点で、「どうやらPersonActivityはmodeIdというパラメータを受け取ることもできるらしい」ということに自然な流れで気付くことができます。

javadocでも第3引数の存在に言及しておくと、さらに親切ですね。

スクリーンショット 2015-05-16 18.10.59.png

しかしこの時点では、第3引数にどんな数値を入れたらいいのか分かりませんね?

まあ適当に 1000 とか入れちゃってみましょう。

スクリーンショット 2015-05-16 18.36.38.png

おや、怒られましたね。マウスオーバーしてみると・・・

スクリーンショット 2015-05-16 18.35.37.png

「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) {


こういったアノテーションを用意しておくと、入力制限はもちろん、補完にも効いてきます。

スクリーンショット 2015-05-16 18.47.57.png


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に空リプを送るなどしてご連絡ください。


追記

startActivity(HogeActivity.getCallingIntent(params))

みたいに呼び出せるやりかたですね。Intentを弄れる余地を残したい場合にはgetCallingIntentのほうが良さそうですねー。


追記2

おっおっ( ^ω^)

createIntent派がのほうが強い感じがしてきました