AndroidのfindViewByIdを考えてみた
AndroidのAPIのうち、最もよく利用するメソッドの一つfindViewByIdについて、色々と思うところがあるので、まとめます。
そもそもfindViewByIdとは
考えられる利用シーンは、XMLで定義してレイアウト中のウィジェットにidを割り振り、それをJavaのコード側で操作をする際に利用するケース。
Activity#findViewByIdを利用して表示しているウィジェットを取得したり、View#findViewByIdで生成したViewの中のViewを取得することがよくあると思います。
また、引数に与える id もプログラマが勝手に作るものではなく、レイアウト定義ファイルの id 属性で自動的に生成するように指定して、AndroidSDKが自動生成するR.javaで定義された物を利用するケースが一般的だと思われます。
さて、ここまでは良いのですがfindViewByIdの返り値の型は単なるViewでしかなく、これがいろいろとイヤな感じのコードを生み出すイメージです。
例えば、レイアウトでボタンの定義を行った場合は、
final Button b = (Button)findViewById(R.id.button); b.setText("i am button");
一行目の用に、キャストを行う必要が出てきます。非常に気持ち悪い。
キャスト失敗って実行時例外なので、例えばレイアウトだけ修正をしてJavaのコードを修正しなかった場合でも動作を行わせるまで問題を見つけることができないんですよね。
あと、単純にキャストって構文として気持ち悪い。あ、これは好みです。
さて、そんなわけで他のフレームワークがどういうことをしているのかちょっと調べてみました。
Xamarin
var button1 = FindViewById<Button> (Resource.Id.buttonCalc);
とりあえずJava以外でAndroidと言えばってことでXamarinです。なんかジェネリクス使ってて良いですね。
ジェネリクス使っているのはいいんですが、結局やってることはキャストと同じなんじゃないかなぁ?Xamarinのコードの読み方が分からんので放置です。
Scaloid
// app.scala def find[V <: View](id: Int): V = basis.findViewById(id).asInstanceOf[V] // your application code find[Button](R.id.login)
find !!!ここもジェネリクスを使ってますが、結局ライブラリの中では、 asInstanceOf なんですね。結局はキャストなので ClassCaseException が起こる可能性があります。
俺が考えたやつ
object Conversions { implicit class View2Opt(val view : View) extends AnyVal{ def asOpt[T <: View](implicit tag : ClassTag[T]) : Option[T] = { view match { case v : T => Some(v) case _ => None } } } } findViewById(R.id.viewPager).asOpt[ViewPager].foreach(_.setAdapter(adapter))
最後に紹介するのはなんと俺が考えたやつ。findViewByidで見つけたViewを結局Optionでラップする方式、実行時例外が発生することもなく、動くときは動くし、動かない時は何も言わずに動く。
クラッシュすることはないけど、正常な動作もしない可能性があります。
結論
findViewByIdってViewで実装されているメソッドだし、XMLとJavaの間で型を結びつける方法が無いので仕方ないんですよね。
あと、キャストに失敗してクラッシュするパターンも経験上あんまりないので問題ないと思ってます。
じゃあなんでこんなネタなのかって言うと、Scalaで書いてるからなんですよね。ScalaでasInstanceOfとかをあんまりやりたくなかった。ただただそれだけ。