これは スピカ Advent Calendar 2015 の15日目(12/15)の投稿です。
以前、Roppongi.aar #1 にて下記ような発表をさせて頂きました。
スライド(SlideShare):Activity/FragmentからControllerへ処理を委譲する
参考実装(GitHub):hkusu/android-controller-delegate-sample
発表の内容は上記のリンクからスライドを見てもらえればよいのですが、かいつまむと、大きくなりがちな Acitivity/Fragment の処理を別クラス(このスライドでは便宜的に Controller
と読んでいます)へ分離してスッキリさせましょう、という内容になっています。
この方法がいいなと思ってる点は、既に太った Activity/Fragment が存在する場合に、既存の構成を大きく変えずに Activity/Fragment のダイエットが出来る点です。
上記のスライドおよび参考実装では Butter Knife の利用を前提としていますが、別に Butter Knifie を利用せずとも普通に View(の参照)を Controller クラスに渡す、という感じでも構わない。
私が開発に携わっている Android版ネイルブック では更に発展させ、Controller
用の基底クラスを用意して利用しています。今回はこの方法を紹介したいと思うのですが、プロダクトのソースコードは載せられないので、説明用の参考実装をこちらに用意しました。
⇒ hkusu/android-butterknife-viewcontroller-sample
この参考実装の中から、主要なコードを抜粋して説明したいと思います。
ちなみにタイトルの「委譲」は Java での「委譲」と同じ意味合いです。英語で表記するなら delegate と書けそうですが、iOS の delegate と混同しそうなので避けています。
今回の前提
- この参考実装では
Controller
をViewController
と呼称しています。- iOS の
ViewController
と混同しそうですが、単にController
だと他のController
的な役割のクラスと区別しづらい為です。
- iOS の
- Butter Knife を利用します。
- Butter Knife のバージョンは執筆時点の最新バージョンである
7.0.1
です。
- Butter Knife のバージョンは執筆時点の最新バージョンである
- 今回は Activity を対象にしています。
- Fragment 用の
ViewController
も、同じような感じで作れると思います。
- Fragment 用の
- 参考実装のコードはプロダクトで利用しているものそのままではなく、個人的に汎用的に利用できるよう再整理したものです。宜しければご自由にご利用ください。
2015.12.16追記:
- Activity から
ViewController
でなく Fragment を使えばいいという話もありますが、レイアウト or コードで画面に配置しないといけないのと、ライフサイクル等の管理が必要になり面倒なので私は避けてます。 - この
ViewController
と MVPアーキテクチャのPresenter
の概念の違いはそれほどありません。まあ同じようなものだと思います。
ButterKnifeViewController クラス
Activity の処理をいい単位(管理しやすい単位/意味的に人が分かりやすい単位)で ViewController
へ切り出すのですが、この ButterKnifeViewController
クラスは各 ViewController
の基底クラスです。
// ...
public class ButterKnifeViewController<T> {
private WeakReference<Activity> mActivityRef;
private WeakReference<T> mListenerRef;
@Nullable
@CheckResult
protected final Activity getActivity() {
if (mActivityRef == null) {
return null;
}
return mActivityRef.get();
}
@Nullable
@CheckResult
protected final T getListener() {
if (mListenerRef == null) {
return null;
}
return mListenerRef.get();
}
@CallSuper
public void onCreate(@NonNull Activity activity) {
ButterKnife.bind(this, activity);
mActivityRef = new WeakReference<>(activity);
}
@CallSuper
public void onStart(@Nullable T listener) {
if (listener != null) {
mListenerRef = new WeakReference<>(listener);
}
}
protected void onResume() {}
protected void onPause() {}
protected void onStop() {}
@CallSuper
public void onDestroy() {
ButterKnife.unbind(this);
mActivityRef = null;
mListenerRef = null; // onStopで処理したいがそのためにコールさせるのは冗長なので
}
}
Butter Knife 周りの定形処理の他に、Activity を渡しています(Android のビュー周りのプログラミングでは何かと Activity が必要な為)。またジェネリクスを利用して、任意のコールバック用のリスナーを設定できるようにしてあります。
Activity とリスナーの実体が一致する可能性がありますが、役割として別物なので分けてます。
◯◯ViewController クラス
ButterKnifeViewController
クラスを継承して各 ViewController
クラスを作成します。私の場合は、クラス名は ◯◯ViewController とするルールにしています。
public class UserEventViewController
extends ButterKnifeViewController<Void> {
// ...
}
ViewController
へ Activity のレイアウトが持つ View がバインドされるので、普通に Butter Knife の記法でインスタンスフィールドに View を定義します。
public class UserEventViewController
extends ButterKnifeViewController<Void> {
@Bind(R.id.toolbar)
Toolbar mToolbar;
@Bind(R.id.todoEditText)
EditText mTodoEditText;
@Bind(R.id.createButton)
Button mCreateButton;
@Bind(R.id.countTextView)
TextView mCountTextView;
@Bind(R.id.todoListView)
ListView mTodoListView;
// ...
後は普通にバインドした View に対してプログラミングしていきます(Acitivity でのプログラミングと一緒です)。Acitivityのライフサイクルメソッドに応じたタイミングで実行したい処理したいであれば ButterKnifeViewController
の onCreate()
onStart()
etc.. をオーバーライドしてその中に処理を記述します。
@Override
public void onStart(@Nullable Void listener) {
super.onStart(listener);
// ここに Activity の onStart で実行したい何かしらの処理
mCreateButton.setEnabled(false);
// ...
}
View にイベントを設定したい場合は普通に View にイベントリスナーを設定してもいいですし、Butter Knife でリスナーを設定してもよいです。
@OnClick(R.id.createButton)
public void onCreateButtonClick() {
// ...
}
Activity が必要な場合は getActivity()
で取得します(必ずnullチェックする)。例えばソフトウェアキーボードを隠す場合は、次のようになります。
Activity activity = getActivity();
if (activity == null) {
return;
}
((InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(mTodoEditText.getWindowToken(), 0);
もし ViewController
から Activity へコールバックで何かしらの通知をする場合は、ViewController
内でインタフェースを定義し、ButterKnifeViewController
継承時にジェネリクスで作成したインタフェースを指定します。
public class UserEventViewController
extends ButterKnifeViewController<UserEventViewController.Listener> {
// ...
public interface Listener {
void onCreateButtonClick();
void onDeleteButtonClick();
}
// ...
}
あとはコールバックしたいタイミングでリスナーのメソッドをコールします。
Listener listener = getListener();
if (listener != null) {
listener.onCreateButtonClick();
}
Activity
Activity から ViewController
を利用します。
まず Activity のインスタンスフィールドで ViewController
のインスタンス化します。
private UserEventViewController mUserEventViewController = new UserEventViewController();
Activity の onCreate()
で ViewController の onCreate()
をコールします。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
mUserEventViewController.onCreate(this);
// ...
}
以降もそうなのですが、Activity のライフライクルメソッド名と
ViewController
のライフサイクルメソッド名を一致させて分かり易いようにしています。
もし ViewController
から Activity へコールバックで何かしらの通知を受ける場合は onStart()
でリスナーを渡します。
@Override
protected void onStart() {
super.onStart();
// ...
mUserEventViewController.onStart(new UserEventViewController.Listener() {
@Override
public void onCreateButtonClick() {
// ...
}
@Override
public void onDeleteButtonClick() {
// ...
}
});
// ...
}
この例では匿名クラスでリスナーを作成していますが、Activity 自体にリスナーのインタフェースを実装(implements)して
mUserEventViewController.onStart(this)
としてもよいです。好みで。
Activity の onDestroy()
で ViewController
の onDestroy()
をコールします。これを忘れると ViewController
内のインスタンス変数に色々な参照が残ってしまうので注意です。
@Override
protected void onDestroy() {
super.onDestroy();
// ...
mUserEventViewController.onDestroy();
// ...
}
以上となります。Activity を見てみると ViewController
に処理が分離している分、コード量が減りスッキリしているのが分かるかと思います。今回は Activity から利用する ViewController
は1つでしたが、ViewController
を複数作成して利用しても構いません(管理しやすい単位/意味的に人が分かりやすい単位で作成するのが大事)。
また今回のように ViewController
の基底クラスを用意することで、各 ViewController
のコードの可読性が良くなります。
おわりに
Activity の処理を別クラスへ分離する方法を説明しました。再掲となりますが、出来るだけ分かりやすいようコメントもいれていますので、興味があれば参考実装もご覧ください。
⇒ hkusu/android-butterknife-viewcontroller-sample
もしゼロから開発するなら MVP やクリーンアーキテクチャ等の構成の方が良いかもしれません。ただそれらのアーキテクチャは慣れるまで時間がかかりますし、今回の方法は既存のAndroid のコードに導入すること、または、既存の Android の作り方を大きく変えないことに重きを置いています。
また処理をカスタムビューへ分離する方法もあります。
⇒ Activity, Fragment, CustomView の使い分け - マッチョなActivityにさよならする方法
画面をいい単位でパーツ化できるなら、カスタムビューでも良いと思います。
個人的にはカスタムビューはちゃんと設計しないと可読性が悪くなってたりして使いづらく、またパーツ同士の連動を考えると複雑になってしまったりするので、あまり使ってません(テクニックが無いだけという説もありつつ..)。
私は スピカ Advent Calendar 2015 で残り2日ほど記事を担当します。他のメンバーの投稿と合わせてどうぞ宜しくお願いします^^