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で使える
Android用DIコンテナである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の指定が必須な点です。あと細かい部分ではbuttonをprivate指定できること(もっと言うと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のインジェクションを比較的簡単に実装できる