Edited at

やさしい設計 〜 Android 編

More than 5 years have passed since last update.


アプリを作っていてありがちなこと

Android には、画面を構成するための Activity というコンポーネントがあり、概ね MVC フレームワークの Controller に相当する機能を持っています。

MVC といえば、肥大化する Controller というのがよくある問題として挙げられますが、Activity も例に漏れず、往々にして肥大化しがちです。

また、Model も、その責務を詰め込んでいくと肥大化しやすいレイヤと言えます。

この投稿では、Controller や Model の肥大化を極力防ぐためのレイヤわけを、Android アプリ向けに書いていきます。


Activity を綺麗に保つ

Activity は、Controller として、様々な UI から受けるイベントを受けて、適切にハンドリングする役割を持っています。

OptionsMenu や ContextMenu のハンドリングを始め、ボタンが押された時や、テキストが入力された時のイベント処理もハンドリングします。


イベントハンドリングにおける条件分岐を整理する

例えば、OptionsMenu や ContextMenu、あるいは、onActivityResult のハンドリング処理は、switch 文等で Activity にベタ書きしていくと、項目が増えるに従って分岐も増えていきます。

// メニューの項目が増えるほどに条件分岐が複雑化する例

public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_hoge:
doHoge();
return true;
case R.id.menu_fuga:
doFuga();
return true;
default:
return super.onOptionsItemSelected(item);
}
}

private void doHoge() {
Toast.makeText(getApplicationContext(), R.string.message_menu_hoge, Toast.LENGTH_SHORT).show();
}

private void doFuga() {
Toast.makeText(getApplicationContext(), R.string.message_menu_fuga, Toast.LENGTH_SHORT).show();
}

Activity にすべてが詰まっているため、テストもしづらくなりますね。

以下のように整理すると、Activity に記述する内容が最小限になります。

// =========== Activity

public class MyActivity extends Activity {
// ... {snip} ...
public boolean onOptionsItemSelected(MenuItem item) {
MyActivityOptionsItemAction action = MyActivityOptionsMenuAction.valueOf(item);
return action.getHandler().handle(this, null) || super.onOptionsItemSelected(item);
}
}

// =========== Options Menu Event enum
// この enum に、MenuItem の id とイベントハンドラとの対応付けをもたせ、Activity から id をもとに引いてきてイベントハンドリングをデリゲートする
public enum MyActivityOptionsItemAction {
HOGE(R.id.menu_hoge, new HogeActionHandler()),
FUGA(R.id.menu_fuga, new FugaActionHandler()),
UNKNOWN(-1, new UnknownActionHandler()); // MyActivity では取り扱っていないはずの id が来た時に使う、NullObject

private final int mMenuId;
private final OptionsItemActionHandler mHandler;

private MyActivityOptionsItemAction(final int menuId, final OptionsItemActionHandler handler) {
mMenuId = menuId;
mHandler = handler;
}

public static MyActivityOptionsItemAction valueOf(MenuItem item) {
for (MyActivityOptionsItemAction action : values()) {
if (action.getMenuId() == item.getItemId()) {
return action;
}
}
return UNKNOWN; // 知らない id が来るのは実装上の不具合なので、IllegalArgumentException を投げるでも良い
}

public int getMenuId() {
return mMenuId;
}

public OptionsItemActionHandler getHandler() {
return mHandler;
}
}

// =========== Interface
public interface OptionsItemActionHandler<E> {
public boolean handle(Context context, E entity);
}

// =========== impl
public class HogeActionHandler implements OptionsItemActionHandler<Void> {
@Override
public boolean handle(Context context, Void entity) {
// menu_hoge を選択された時の処理
Toast.makeText(context.getApplicationContext(), R.string.message_menu_hoge, Toast.LENGTH_SHORT).show();
return true;
}
}

public class FugaActionHandler implements OptionsItemActionHandler<Void> {
@Override
public boolean handle(Context context, Void entity) {
// menu_fuga を選択された時の処理
Toast.makeText(context.getApplicationContext(), R.string.message_menu_fuga, Toast.LENGTH_SHORT).show();
return true;
}
}

public class UnknownActionHandler implements OptionsItemActionHandler<Void> {
@Override
public boolean handle(Context context, Void entity) {
return false;
}
}


コールバックインタフェースを減らす

特に Model の実装時に多いのが、非同期処理後のコールバックインタフェースの乱立です。

以下のように、Loader を用いた非同期処理を含む Model クラスの実装をあちこちでしていくと、モデルのコールバックが大量に出来上がります。

public class MyModel implements LoaderCallbacks<Something> {

private static final int LOADER_ID = 0;
private Context mContext;
private MyModelCallback mCallback;

@Override
public Loader<Something> onCreateLoader(int id, Bundle args) {
return new AsyncSomethingLoader(mContext);
}

@Override
public void onLoadFinished(Loader<Something> loader, Something result) {
if (mCallback == null) {
return;
}

if (result == null) {
mCallback.onFailure();
} else {
mCallback.onSuccess(result);
}
}

@Override
public void onLoaderReset(Loader<Something> loader) {}

public void onCreate(Context context, MyModelCallback callback) {
mContext = context;
mCallback = callback;
}

public void load(LoaderManager manager) {
manager.initLoader(LOADER_ID, null, this);
}

public void onDestroy(LoaderManager manager) {
mCallback = null;
manager.destroyLoader(LOADER_ID);
}

public interface MyModelCallback {
public void onSuccess(Something something);
public void onFailure();
}
}

コールバックはまた、実装する Context のライフサイクルを考慮した管理をしなければなりません。

これらの面倒事を解消する策として、EventBus を使います。

以下、このライブラリを用いた実装を例にあげてみます。

public class MyModel implements LoaderCallbacks<Something> {

private static final int LOADER_ID = 0;
private Context mContext;
private EventBus mEventBus;

@Override
public Loader<Something> onCreateLoader(int id, Bundle args) {
return new AsyncSomethingLoader(mContext);
}

@Override
public void onLoadFinished(Loader<Something> loader, Something result) {
if (result == null) {
mEventBus.post(new SomethingLoadFailureEvent())
} else {
mEventBus.onSuccess(new SomethingLoadSuccessEvent(result));
}
}

@Override
public void onLoaderReset(Loader<Something> loader) {}

public void onCreate(Context context) {
mContext = context;
mEventBus = EventBus.getDefault();
}

public void load(LoaderManager manager) {
manager.initLoader(LOADER_ID, null, this);
}

public void onDestroy(LoaderManager manager) {
manager.destroyLoader(LOADER_ID);
}
}

コールバックのインスタンスを管理する部分がごっそりなくなりました。かわりに、イベントを受け取る実装を Activity ですることになります。

より詳しく EventBus の使い方を見るには、こちらを御覧ください。


Model のなかの棲み分け

Model と一言で言っても、その役割はたくさんあります。

ネットワークや DB 、キャッシュなどからデータを非同期で取ってきて、それをデシリアライズ、加工し、View に反映させるまでの間にも、いくらかのビジネスロジックを挟んだりします。

大まかに分類すると、それぞれ、データにアクセスするレイヤ、データへのアクセスを非同期にするためのレイヤ、データを加工したりメモリにのせたりするレイヤ、に分けるとすると、以下のような名前空間に分離することができます。


  • client: データにアクセスするレイヤ

  • loader: Android における Loader を用いた非同期処理のレイヤ

  • model: データを加工したりメモリにのせたりする、所謂ビジネスロジックを含むモデルのレイヤ

さらに、データそのものを表現するためのオブジェクトとして、Entity を定義することによって、Model や Controller、View などのコンポーネントを Entity が行き来するようなアーキテクチャが構成できます。

モデルも細かく分離することで、テストが容易になります。