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のインジェクションを比較的簡単に実装できる