ViewからFieldへの代入をアノテーションで自動化する

AndroidGUIXMLを使用した宣言的な構成を可能としており、そのためのコードが不要なのは素晴らしいが、それでもアプリケーション内でGUIを参照しようとすると、以下のようなコードを書く必要がある。

public class FooActivity extends Activity {
    protected TextView a;
    protected TextView b;
    protected TextView c;
    protected TextView d;
    protected TextView e;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.main);
        :
        this.a = ((TextView)findViewById(R.id.a));
        this.b = ((TextView)findViewById(R.id.b));
        this.c = ((TextView)findViewById(R.id.c));
        this.d = ((TextView)findViewById(R.id.d));
        this.e = ((TextView)findViewById(R.id.e));
        :
        :
    }

ならばと対象になっているActivityのフィールドに対して、宣言的にコンテキストやリソースからオブジェクトを注入できると、後々便利だろうと表題の件を考えてみた。

対象はAndroidで使用可能な全てのリソース(Androidリソースはハンドルでアクセスする一種のリポジトリともいえる)と言いたい所だが、最初からあまり大げさなものを書くのも嫌なので、まずはViewだけを対象にする。

Viewフィールドに対して、Rクラスのidを直接指定するためだけのアノテーションを用意する

@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
    int id() default 0;
}

今回は必要最低限の属性であるidだけに留めたが、一般的なプロパティ(テキスト内容、桁数、文字色等)を設定できるように、属性に追加するのも良いだろう。

以下、実際にアノテーションをパースしてリソースからビュー・フィールドへ参照を注入する"ViewInjector"クラスの実装の叩き台だ。

  • ViewFieldInjector.java
public final class ViewFieldInjector implements IInjectable {
    protected final Activity activity;

    public ViewFieldInjector(Activity activity) {
        this.activity = activity;
    }

    public void inject() {
        this.collectAnnotations(this.activity);
    }

    private void collectAnnotations() {
        Field[] fields = this.activity.getClass().getFields();
        for ( Field field : fields ) {
            field.setAccessible(true);
            try {
                Inject inj = field.getAnnotation(Inject.class);
                if ( inj != null ) {
                    if ( inj.id() != 0 ) { //idあり
                        this.injectViewToTarget(field, inj.id());
                    }
                }
            } catch (Exception e) {
                Log.e(this.getClass().getName(), e.toString());

            }
        }
    }

    private void injectViewToTarget( Field field, int handle) throws IllegalArgumentException, IllegalAccessException {
        View view = this.activity.findViewById(handle);
        if ( view != null ) {
            field.set(activity, view);
        }
    }
}

処理的には簡単で、Activityの型情報を利用してフィールドに記述されているアノテーションを処理しているだけ。リフレクションを使うため、性能では不利になる。

最終的には、Activityクラスのフィールドを以下のように記述することを想定している。

public class FooActivity extends Activity {
    @Inject(id=R.id.a)
    protected TextView a;
    
    @Inject(id=R.id.b)
    protected TextView b;
    
    @Inject(id=R.id.c)
    protected TextView c;
    
    @Inject(id=R.id.d)
    protected TextView d;
    
    @Inject(id=R.id.e)
    protected TextView e;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.main);
    }

コードは上記のようにかなりすっきりする。

問題はこのViewFieldInjectorをどの時点で動作させるかだ。

ActivityのonCreateハンドラが一番無難なのだろうが、このような陰の仕組みにはアプリケーションコードを依存させたくない。

  • これはできれば避けたい
public class FooActivity extends Activity {
    :
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.main);
        
        //インジェクタを生成
    ViewFieldInjector injector = ViewFieldInjector(this);
    injector.inject;
    }

可能であればACTION_BOOT_COMPLETEDに反応するインテントのように、アクティビティの起動時に割り込む処理を書きたいんだけどな。