Android

太ったActivity/Fragmentをダイエット!Controllerへ処理を委譲する

More than 3 years have passed since last update.

これは スピカ Advent Calendar 2015 の15日目(12/15)の投稿です。

以前、Roppongi.aar #1 にて下記ような発表をさせて頂きました。

スクリーンショット 2015-12-14 22.14.36.png


スライド(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

スクリーンショット 2015-12-16 1.06.34.png

この参考実装の中から、主要なコードを抜粋して説明したいと思います。


ちなみにタイトルの「委譲」は Java での「委譲」と同じ意味合いです。英語で表記するなら delegate と書けそうですが、iOS の delegate と混同しそうなので避けています。



今回の前提


  • この参考実装では ControllerViewController と呼称しています。


    • iOS の ViewController と混同しそうですが、単に Controller だと他の Controller 的な役割のクラスと区別しづらい為です。



  • Butter Knife を利用します。


    • Butter Knife のバージョンは執筆時点の最新バージョンである 7.0.1 です。



  • 今回は Activity を対象にしています。


    • Fragment 用の ViewController も、同じような感じで作れると思います。



  • 参考実装のコードはプロダクトで利用しているものそのままではなく、個人的に汎用的に利用できるよう再整理したものです。宜しければご自由にご利用ください。

2015.12.16追記:


  • Activity から ViewController でなく Fragment を使えばいいという話もありますが、レイアウト or コードで画面に配置しないといけないのと、ライフサイクル等の管理が必要になり面倒なので私は避けてます。

  • この ViewController と MVPアーキテクチャの Presenter の概念の違いはそれほどありません。まあ同じようなものだと思います。


ButterKnifeViewController クラス


ソースはこちら(GitHub)


Activity の処理をいい単位(管理しやすい単位/意味的に人が分かりやすい単位)で ViewController へ切り出すのですが、この ButterKnifeViewController クラスは各 ViewController の基底クラスです。


ButterKnifeViewController.java

// ...

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 クラス


ソースはこちら(GitHub)


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のライフサイクルメソッドに応じたタイミングで実行したい処理したいであれば ButterKnifeViewControlleronCreate() 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


ソースはこちら(GitHub)


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()ViewControlleronDestroy() をコールします。これを忘れると 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日ほど記事を担当します。他のメンバーの投稿と合わせてどうぞ宜しくお願いします^^