Help us understand the problem. What is going on with this article?

やさしい設計 〜 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 が行き来するようなアーキテクチャが構成できます。

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

KeithYokoma
Android, Java, Perl, Scala, Play Framework, CoffeeScript, Smalltalk, JavaFX, Groovy, AWS, Docker
http://keithyokoma.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away