データバインドの実装(2 イベントハンドラの分離)

AndroidアプリケーションにおいてMVCの扱い、特にActivityの役割をどう考えるかは必ず議論になるが、私はActivityはできるだけビューと考えてロジックは書かず、モデル側もビューとの依存性はもたないように(単独でテストできるように)設計、実装する。
[Android][SDK]データバインドの実装(1)

という訳でActivityにはできるだけロジックを書きたくない。とはいっても純粋なビューとは違うので全くコードを書かない訳にもいかないので、以下のポリシでコードを分離することにしている。

[アクション]
ビジネスロジックを起動する、ビューの内容をモデルに転記する(データバインドも含む)等、Contextのコントローラとしての処理は"アクション"とし、別なクラスに分離する。※

[ギミック]
上記以外のGUI操作。例えばボタンを押下することで行を追加する、カレンダの日付を変える等、Activity中に閉じたGUI操作に関する処理は"ギミック"とし、通常通りActivity中に記述する。(GUIの無いサービスにはギミックは存在しない)

このような設計はいたずらにその仕組みを複雑にすることでCPUやリソースを余計に消費する懸念があり前回のデータバインド(1)で書いた口上と矛盾するかもしれないが、無名クラスでイベントハンドラを書くのとそれほど変わらないとも考えられる。異論もあるだろうがWindowsForms(C#)、JFC/Swingと一環して同様のクラス設計をしてきており、今回も同様に設計する。


実際のアクションの分離だが、以下の順序で実装する。

  • 1. POJOでアクションクラスを用意する

これは何も説明することは無いだろう。以下のようにビジネスロジックを起動したり、ビューの内容をモデルに転記したりする処理を単純なJavaクラスで実装する。

public class AddressDroidAction {
    public void insert(AddressInfo info) { 
        PersistentEngine engine = PersistentEngine.getInstance(AddressInfo.class);
        engine.insert(info);
    }
    public void index(AddressInfo info) { 
        PersistentEngine engine = PersistentEngine.getInstance(AddressInfo.class);
        AddressInfo[] infoArray = engine.query(info);
        :
        //View側に転記
    }
}

Context側のイベント処理を外部に分離するために、Context上で発生したイベントを捕捉してアクションクラスのメソッドに接続する必要がある。無論コードで動的に行うのだが、コードを分離するために別なコードを書くのは本末転倒なのでアノテーションの助けを借りる。

public class AddressDroidAction {
    /**
     * 本当ならアプリケーションに複数のActivityが存在する場合を考慮しなければならないのだが、ここでは省略している。
     *
     */
    @Execute(viewId = R.id.Button01, eventId = EventId.EVENT_ID_CLICK )
    public void insert(AddressInfo info) { 
        PersistentEngine engine = PersistentEngine.getInstance(AddressInfo.class);
        engine.insert(info);
    }
    @Execute(viewId = R.id.Button02, eventId = EventId.EVENT_ID_CLICK )
    public void index(AddressInfo info) { 
        PersistentEngine engine = PersistentEngine.getInstance(AddressInfo.class);
        AddressInfo[] infoArray = engine.query(info);
        :
        //View側に転記
    }
}

Executeアノテーションはイベントを補足する対象のビューとイベントを識別するためのイベントIDを記述する。AndroidGUIイベントはJFC/SwingのようにIDで管理されていないので、そのための定数クラスを用意して一意に決定できるようにしている。

なお、Executeアノテーションの対象となったメソッドのパラメタに渡される引数は、前回書いた方針でデータバインドが実行されるため、Viewと結合したオブジェクトと一部の予約オブジェクト(ApplicationオブジェクトやAndroidの各種マネジャオブジェクトの場合は内容が自動的にセットされる。

public final class EventId {
    public static final int EVENT_ID_START = 1001;
    public static final int EVENT_ID_BUFFERINGUPDATE = EVENT_ID_START + 1;
    public static final int EVENT_ID_CANCEL = EVENT_ID_START + 2;
    public static final int EVENT_ID_CHANGE = EVENT_ID_START + 3;

    public static final int EVENT_ID_CHECKEDCHANGE = EVENT_ID_START + 4;
    public static final int EVENT_ID_CHECKEDCHANGEWIDGET = EVENT_ID_START + 5;
    public static final int EVENT_ID_CHILDCLICK = EVENT_ID_START + 6;
    public static final int EVENT_ID_CHRONOMETERTICK = EVENT_ID_START + 7;
    public static final int EVENT_ID_CLICK = EVENT_ID_START + 8;
    :

AddressDroidActionはその生成時にExecuteアノテーションの有無を判定して、EventMapper#attachEventListenerForActionにより、Activity下のビューのイベントに動的に接続される。

public final class EventMapper {
    private static final String PREFIX_SETTER = "set";
    
    public final Object attachEventListenerForAction(
            final int eventId, final IAction action, final Object target ) {
        
        switch (eventId) {
        case EVENT_ID_CLICK:
            return setSuitableLister(eventId, new OnClickListener() {
                @Override
                public void onClick(View v) {
                    action.invoke(eventId, new Object[]{v});
                }
            }, null, target);
        :
        :
        :
    }
    private final Object setSuitableLister(int eventId
            , Object el, String listenerAliasName, Object component) {
        Class listenerInterface = el.getClass().getInterfaces()[0];
        String listenerName = StrUtil.isNotEmpty(listenerAliasName)
                    ? listenerAliasName
                    : listenerInterface.getSimpleName();
        Class clazz = component.getClass();
        String setMethodName = PREFIX_SETTER + listenerName;
        //リスナのセッター起動
        Method setMethod = ClassUtil.getMethodNoException(clazz
                , setMethodName, listenerInterface);
        MethodUtil.invoke(setMethod, component, el);
        return el;
    }

コードは処理の雰囲気を見せる為の断片であり、動作可能なものではないのであしからず(関連メソッドまで含むと膨大になってしまうので省いている)

attachEventListenerForActionメソッドのパラメタeventIdはアノテーションで指定されたイベントを識別するID、IActionはPOJOアクションクラスから生成したインタフェースであり@Executeアノテーションを記述したメソッドの情報を保持している。targetはイベントハンドラを持つオブジェクト(大抵はViewインスタンス)である。必ずしもイベントハンドラのセッターがpublicになっていないため、ここでもリフレクションを使用してる。とほほだ。

これで曲がりなりにもアクションをActivity/Serviceから分離することができた訳だ。

なお、以上は私が既に実装しているもの、これから実装しようと妄想しているものが入り交じった記録、備忘録なのであしからず。

.NET Frameworkの「コードビハインド」に近いかもしれない。