算譜王におれはなる!!!!

偏りはあると思うけど情報技術全般についてマイペースに書くよ。

KotlinでAndroidアプリ開発: Viewのインジェクションを実現する

AndroidAnnotationsはKotlinでは使えない

アノテーションを付けることでボイラープレートなコードを削減するツールであるAndroidAnnotationsは僕にとってもはやAndroidアプリ開発で欠かせないツールとなっています。AndroidAnnotationsを使ったサンプルコードとしては下記のように自動でViewをインジェクトする例が代表的だと思います。

@EActivity(R.layout.activity_main)
public class MainActivity extends Activity {
    
    @ViewById
    Button button;
}

AndroidAnnotationsのようなツール/ライブラリを使わない場合はbutton = (Button) findViewById(R.id.button);を書く必要があって、キャストがなんか嫌だし、リソース名とフィールド名が同じ場合は重複して指定してる感じがあって体がムズムズしますね。

このように非常に便利なAndroidAnnotationsですが、残念ながら、みんな大好きKotlinでは使えないようなのです。

RoboGuiceはKotlinで使える

AndroidDIコンテナであるRoboGuiceにもViewをインジェクトする機能があります。上記のコードをRoboGuiceを使って書くと次のようになります。

public class MainActivity extends RoboActivity {
    
    @InjectView(R.id.button)
    Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

AndroidAnnotationsとの違いはRoboActivityを継承する点とリソースIDの指定が必須な点です。あと細かい部分ではbuttonprivate指定できること(もっと言うとfinalも付けられる)。これはAndroidAnnotationsがAPTで静的にやってることと、RoboGuiceがリフレクションで動的にやっていることの違いですね。それぞれ一長一短あると思います。

で、このRoboGuiceはKotlinでも使えます*1

KotlinでRoboGuiceを使ってみる

同じ例をKotlin + RoboGuiceで書いてみます。

class MainActivity: RoboActivity() {

    [InjectView(R.id.button)]
    val button: Button? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

まぁだいたいいい感じですが、3つ気になるところが。

  • AndroidAnnotaionsみたいにリソースIDを省略したい
  • リソースが見つからない場合はNPEが投げられるにも関わらず、プロパティはNullableにせざるを得ない
  • できればRoboActivityは継承したくない

KotlinでViewをインジェクトするAPIつくった

ということで本題です。上記の不満を解消すべく、ViewをインジェクトするAPIを作ってみました。この作ったやつを使うと今まで示した例と同じ内容を次のように書けます。

class MainActivity: Activity() {

    val button: Button by viewInjector()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

これにより不満が解消されました。

  • リソースIDを省略できるようになった
  • NotNullにできる
  • ライブラリ独自のActivityを継承せずに済んだ

解説

一言で説明すると「拡張関数とDelegated Proptertyとリフレクションを使って実現した」です。

まず拡張関数についてですが、viewInjector()Activityの拡張関数として定義しました。viewInjector()はDelegated Property機能を提供するオブジェクトを返します。

Delegated Propertyとは平たく言えば、プロパティへのアクセスを他のオブジェクトへ委譲する仕組みのことです。この仕組みを使うことでbuttonが最初に参照されたときに、リフレクションを使ってボタンを取得しbuttonへセットします。

コード

public trait Injector<T> {
    fun get(thisRef: Any, prop: PropertyMetadata): T
}

// リソースIDを明示的に指定できるようにもした
public fun <T: View> Activity.viewInjector(resId: Int? = null): Injector<T> = object: Injector<T> {

    private var view: T? = null

    override fun get(thisRef: Any, prop: PropertyMetadata): T {
        // 初回のアクセスのみ初期化する
        if (view == null) {
            view = getView(resId, prop.name)
        }

        return view!!
    }

    private fun <T: View> Activity.getView(resId: Int?, name: String): T =
            (if (resId != null)
                findViewById(resId)
            else
                findViewByName(name)) as T

    // プロパティの名前からViewを取得する
    private fun Activity.findViewByName(name: String): View? =
            try {
                val resIds = Class.forName(getPackageName() + ".R\$id")
                val resId = resIds.getField(name).get(resIds) as Int
                findViewById(resId)
            } catch(e: Exception) {
                null
            }
}

Injectorの種類を増やしたり例外対応したらGithubにあげます。

まとめ

  • AndroidAnnotationsはKotlinでは使えない
  • RoboGuiceはKotlinでも使える
  • Kotlinの機能を使ってViewのインジェクションを比較的簡単に実装できる