Androidアプリの開発に業務で携わってから半年と少しの新米 Android エンジニアです。
Activity の仕事を Object Composition (オブジェクト合成)パターンで減らしたら、予想以上に使い勝手が良かったので、色々な方に使っていただきたいなと思って。
Object Composition すると何が良いのか
大きく分けて2つの効能があります。
Acitvity の役割がはっきりする
Activity に複数の責務を負わせることがなくなるため、 メソッドの一覧を見るだけで Activity が何をしているのかを把握することができます 。
具体的には、 MVCパターンやMVPパターンにおけるViewの役割に徹させることが可能です。
また、それゆえに Activity に書くべきことが少なくなります。
実際に業務で書いた Activity (サーバから受けたデータを表示し、複数種類の導線を持っている)は200行前後になり、数スクロールで中身を把握できるようになりました。
私は Fragment で試してはいないのですが、 Fragment でも同じように、 Viewとして徹させるために Object Composition パターンを使うことができると思います。
同じような機能を複数の画面で使いまわせる
よく使う機能を 複数の画面で取り扱う ことができるようになります。
実際には以下のような機能を Object Composition を行うクラスに切り出したところ、便利に使い回すことに成功しています。
- 写真を撮影 or ギャラリーから選択させ、写真の
Uri
をハンドリングする
// onClick の中
// 「カメラで撮影する」「ギャラリーから選択する」の選択肢が出てくる
pictureComposable.showPictureChooseDialog();
// onActivityResult の中
pictureComposable.onActivityResult(requestCode, responseCode, data, new OnPictureChosenCallback {
@Override
onPictureChosen(Uri pictureUri) {
// 好きなようにハンドリングする
}
...
});
- 検索結果を複数件選択させ(ただし上限値以上は選択不可)、結果をハンドリングする
// onCreate の中
selectionComposable.registerObserver(new DataSetObserver {
@Override
public void onChanged() {
// 要素の選択数が変化したときのUI操作は Activity でハンドリング
}
...
});
// Adapter の OnItemClick などの中
selectionComposable.addItem(item);
// 次の画面へ行くボタンの onClick の中
Serializable selectedItems = selectionComposable.getSelectedItems();
Intent intent = NextScreenActivity.createIntent(this, selectedItems)
後からプロジェクトに参加された方もこのクラスを使って短期間でお仕事を終わらせてくださったので、仕事の効率化にも効果があると思っています。
Object Composition するクラスの作り方
こんな感じで使えるやつを作ります。
命名
〜〜Manager
とか 〜〜Composable
とか、開発者間で合意が取れていれば何でもいいと思います。
ただ、広い意味あいの言葉にすると誤解が生まれやすく、あらゆる役割が押し付けられる可能性があるので、ある程度慎重に行う必要はありそうです。
cf. クラスの命名のアンチパターン
(クラス名が長くても良いのであれば、 〜〜ActivityComposable
とか、そんな接尾詞を使った方が良いのかもしれません)
以下では Activity に Object Composition するクラスには Composable
の接尾詞を付けることにします。
Activity のライフサイクルに添わせる
Activity と同じライフサイクルを辿らせると、管理が楽でとてもいい感じです。
生成と保持
Composable クラスは Activity::onCreate
でインスタンス化し、 Activity のメンバに保持します。
Composable の中で Context
を使いたい場合は、コンストラクタの引数に Activity を渡してメンバに保持するようにすると便利に使えます。
(もちろんですが、引数で受ける型は Activity
や Context
であるべきです。具体的な Activity 名を入れると再利用性がなくなります)
ライフサイクル
Composable クラスに生やすメソッドは、 Activity のライフサイクルメソッドに合わせておくと使いどころがわかりやすくなります。
例えば、アプリがバックグラウンドに潜る際に何か処理をさせたいならonResume
メソッドを作って Activity の同名メソッドから呼ぶようにします。
メンバの保持
Composable クラスは Activity と同じライフサイクルで生き死にするので、メンバに保持した値は onSaveInstanceState
で保存し、 onRestoreInstanceState
で復元するようにする必要があります。
Bundle に保存する際の key は、 Activity や他の Composable クラスと衝突しないように、クラス名などを接頭辞としてつけておくと良いかもしれません。
メソッドを生やす
Activity に置くべきではなく Composable に任せたい処理は、Composable にメソッドを生やして、 Activity から実行するようにします。
Activity を操作したいときは interface を使って、不必要な依存関係を作らない
Composable クラス側から Activity を操作したい場合、 Activity を import してしまうと Composable から Activity への依存が生じてしまいます。
そうなるとこの Composable を別の Activity で使うことができなくなってしまうわけです。
これを防ぐ方法は主に2つあります。
- コールバックやリスナーを interface として定義しておき、 Activity 内の static クラスや匿名クラスとして Activity 側の処理を実装する
// Composable 側
public class HogeHogeComposable {
public interface SomeActionCallback {
// コールバックを作っておく
void done(int result);
}
public void doSomeAction(SomeActionCallback callback) {
...
// 作業が終わったら呼ぶ。結果は引数から渡す。
callback.done(result);
...
}
}
// Activity 側
public class HogeHogeActivty extends Activity {
...
// Composable に処理を任せた結果は Activity で使う
composable.doSomeAction(new SomeActionCallback {
@Override
public void done(int result) {
// 結果を使う
...
- Composable が使うメソッドを interface として定義しておき、 Activity にそれを実装する
// Composable 側
public class HogeHogeComposable {
public interface HogeHogeResultHundler {
// Activity に持たせるメソッド
void showResult(int result);
}
...
// どこかで HogeHogeResultHundler を受け取って使う
delegatable.showResult(result);
...
}
// Activity 側
public class HogeHogeActivity extends Activity implements HogeHogeComposable.HogeHogeResultHundler {
// 実装する
@Override
public void showResult(int result) {
// 結果を使って何かする
...
どちらか使いやすい方を使えば良いと思います。
個人的には前者が好きです。記述量は増えてしまいますが…
どちらを使うにしても、これで不要な依存関係をなくすことができ、作った Composable を複数の画面で使い回すことができます。
EventBus や RxJava を使える環境なら、専用の interface を作らなくても良いかも知れません。
Fragment は?
Activity の仕事を肩代わりさせる先として真っ先に候補にあがる Fragment。
Fragment も View の役割を担うものとして考えるなら、 Fragment でも Object Composition パターンはもちろんそのまま使えます。
(UIと機能が必ず一致するなら Object Composition パターンで切り出す必要もないと思いますが)
記事中の Activity を Fragment に(その他ライフサイクルメソッドも Fragment のものに)読み替えていただければ良いと思います。
作例
最終的にこんな感じになりますよ、というイメージで。
一部の実装はきれいにコード書ける自信がなかったので省略。
public class TakeOrChoosePictureComposable {
// 適当に他のActivityなどと被らない整数値
private static final int REQUEST_CODE_TAKE_PICTURE = ...;
private static final int REQUEST_CODE_LIBRARY = ...;
// メンバの save と restore に必要
private static final String ARG_TEMP_PICTURE_FILE_URI = "temp_picture_file_uri";
// Context や Activity はメンバに抱えておくと良い感じ
private Activity activity;
// 後で save や restore するメンバ
private Uri tempPictureFileUri;
public TakeOrChoosePictureComposable(Activity activity) {
this.activity = activity;
}
public void onSaveInstanceState(Bundle outState) {
outState.putParcelable(ARG_TEMP_PICTURE_FILE_URI, tempPictureFileUri);
}
public void onRestoreInstanceState(Bundle savedInstanceState) {
tempPictureFileUri = savedInstanceState.getParcelable(ARG_TEMP_PICTURE_FILE_URI);
}
public void showPictureSourceChooseDialog() {
// 内容についてはもっとスマートな方法があると思うので参考程度に
AlertDialog.Builder ab = new AlertDialog.Builder(activity);
final CharSequence[] sourceLabels = {
"カメラで撮影",
"ギャラリーから選ぶ"
};
ab.setItems(sourceLabels, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
switch (i) {
case 0:
//他カメラアプリで撮影
startOtherCameraApp();
break;
case 1:
//ギャラリーから画像読込
startGalleryApp();
break;
}
}
});
ab.show();
}
private void startOtherCameraApp() {
...
// 私が知っている方法だと、生成した Uri をどこかに確保しておく必要があるので tempPictureFileUri を使う
...
// // REQUEST_CODE_TAKE_PICTURE を使って activity.startActivityForResult する
}
private void startGalleryApp() {
...
// REQUEST_CODE_LIBRARY を使って activity.startActivityForResult する
}
public void onActivityResult(int requestCode, int resultCode, Intent data, OnPictureTakenCallback callback) {
switch (requestCode) {
case REQUEST_CODE_TAKE_PICTURE:
if (resultCode != Activity.RESULT_OK) {
callback.onFail();
return;
}
callback.onPictureTaken(tempPictureFileUri);
break;
case REQUEST_CODE_LIBRARY:
if (resultCode != Activity.RESULT_OK) {
callback.onFail();
return;
}
Uri distFileUri = data.getData();
callback.onPictureTaken(distFileUri);
break;
}
}
// 結果をハンドルするためのコールバック interface
public interface OnPictureTakenCallback {
void onPictureTaken(Uri distFileUri);
void onFail();
}
}
見通しの良い Acitivty を書きたい(他の人にも書いて欲しい)
現在のお仕事でメンテナンスしているコードはには驚きのコード量を誇るActivityがあったりします。
(ファイル名はイメージ。行数は本当にこの行数でした)
$ find . -name "*Activity.java" | xargs wc -l | sort -r
4536 ./app/src/main/java/com/company/mainteningapp/activity/VeryImportantActivity.java
.
.
.
お世辞にも「見通しが良い」とは言えません。
「コード行数が多くて把握が大変」というだけでなく、以下のようなスパゲティコードが跋扈しているためです。
- UIの状態とサーバーに送るステータスを混同して保持するメンバ変数がいる
- サーバーとやりとりするパラメーターが個別にメンバ変数に持たれている
- しかもそれがあらゆるところでアクセスされており、どんなタイミングで変更されるのかコードからはまるで読めない
- 通信のために繰り広げられる
new Thread { ... }.execute()
とhandler.post(new Runnable { ... })
の深いネスト -
onCreate()
の中に広がる長大なnew OnClickListener { ... }
画面の向こうの諸兄ならこうなってしまった原因はいくつも想像できると思うのですが、私はこうなってしまう一番の原因は
Activityにすべての責務を押し付けること
にあると思っています。
見通しの良いコードを書くには(一般論)
クラスごとに責務を分割し、それ以外のことはしない (関心を分離する)ことが鉄則だと思っています。
関心の分離についてはhirokidaichiさんの以下の記事が詳しいので、そちらを読んでいただけると。
新人プログラマに知っておいてもらいたい人類がオブジェクト指向を手に入れるまでの軌跡
見通しの良いコードを書くには(Android開発の場合)
Android開発の場合は、SDKによって様々な種類のクラスが提供されています。
本来はこれらを正しく使うだけでもクラスごとに責務を分離して、見通しの良いコードを書けるはずなのです!
実際にどうすれば分離できるのかは、あんざいゆきさんのこのスライドの後半に実例があるので、そちらを見ていただけると。
Activity, Fragment, CustomView の使い分け - マッチョなActivityにさよならする方法 -
とはいえSDKが提供しているクラスを全部把握しろと言われましても無理な話ですし、過去の遺産やら時間的制約やら自分の技量などの要因で、新しく実装したい機能で
「とりあえず Activity に書いて試してみよう」
↓
「あ、上手く行っちゃったしこれはこのままでいいや」
のコンボが決まってしまって、書き散らかしたコードが残りつつけることもよくある気がします。
魔法の呪文
そんなときに、一歩だけ踏みとどまって自分に問いかけるのです。
「これはViewに載せるべき処理なのか?」
答えがNoだったら、 Acitivty によく似た Composable クラスをこしらえてその処理をカット&ペーストしてしまいましょう。
それだけで Activity の見通しが良くなって、かつ他の画面でも再利用可能なモジュールが出来上がります。
どうやって関心の分離を実現していいかわからない場合などに、ぜひ Object Composition パターンを活用してみてください。
そして、もっと読みやすいコードをこの世に増やしましょう!(切実