DEV Community

Koji Matsubara
Koji Matsubara

Posted on

2

Kotlin Android Extensions の View Binding を ViewHolder でも良い感じに使うには

はじめに

これまで NewsPicks のエンジニアがまとまった形で外部に対して発表を行なう事が多くなく、なかなか中のエンジニアが普段どのような技術に取り組んだり、悩んだりしているかを知ってもらえる機会が少ないなと感じていました。

そこで今年から NewsPicks でも NewsPicks Advent Calendar 2018 と称して Advent Calendar を始める事としました。

初日は、言いだしっぺである @_koji_matsu_ が主に担当している Android の分野から Kotlin Android Extensions について記事にしたいと思います。

NewsPicks における Kotlin

2018年12月現在 NewsPicksNewsPicks アカデミア の2アプリを Android 向けに提供しています。その内 NewsPicks については新規コードの全てを、 NewsPicksアカデミア については100% Kotlin で開発を行なっています。

実は、 NewsPicks アカデミアのサーバサイドも 100% Kotlin で開発しており、以前弊社の@monzou記事を書いていて大変おもしろい内容なので是非読んでみてください。

View Binding

お手軽に Android で Kotlin を使う場合ぱっとメリットが感じられるのが Kotlin Android Extensions を用いた View Binding ではないかと思います。 DataBinding をがっつり使われている現場であれば、DataBinding で代替できますが、 今 Jake神 の Butter Knife を主に使われている方であれば、Butter Knife のような ViewBinding 処理を Kotlin という言語開発元の JetBrains が用意してくれていると思ってもらえれば良いと思います。

具体的には良くあるレイアウトに定義されている View のインスタンスを取得する以下のようなコードを

final Button button = findViewById(R.id.button);
button.setOnClickListener(view -> {
  // do something
})

こんな感じで書く事ができます。

button.setOnClickListener {
  // do something
}

ここで唐突に出てくる button というインスタンスですが、これが Kotlin Android Extensions の機能で View の ID 名をそのまま対応する変数名に自動的に Bind できる機能になります。

更に、 button 変数に 実際の Button インスタンスを Bind をする際に、毎回 findViewById 行ないパフォーマンスが落ちてしまわないように自動的にインスタンスをキャッシュする機能も持っています。

RecycleView.ViewHolder 内で ViewBinding を使用する場合の問題点

このように、素の Java で Android を開発する場合に発生する findViewById をすっきりさせてくれる ViewBinding ですが、前節で書いたインスタンスをキャッシュする機能は現状、 Activity, Fragment, View 内でという制限が付きます。

つまり、このような 単純な ViewHolder を書いた場合 HogeViewHolder#show() がコールされる毎に R.layout.hogehoge 内の ID R.id.title の View に対して毎回 findViewById が発生する事になってしまいます。(つまり View のインタンスを保持するという ViewHolder としての機能を果たせていない)

class HogeAdapter(private val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    // ViewHolder 生成
    return HogeViewHolder(LayoutInflater.from(context).inflate(R.layout.hogehoge, parent, false))
  }

  override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    // ViewHolder 内の View インスタンスに対して表示を要求する
    (holder as HogeViewHolder).show()
}
  ....

  private inner class HogeViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
    fun show() {
      // これで R.layou.hogehoge 内の R.id.title という View にアクセスはできるが…
      view.title.text = "hoghoge"
    }
  }
}

LayoutContainer Support

このように、通常 ViewHolder 内などで Kotlin Android Extenstions を用いた ViewBinding を行なうと、思わぬ落とし穴が存在しています。

ここで、もやっとする心を抑え、findViewById を使って View のインタンスをきちんと保持する事も当然できます。 例えばこんな感じ

private class HogeViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
  private val title: TextView? = null

  fun show() {
    if (title == null) title = view.findViewById(R.id.title)
    title?.text = "hoghoge"
  }
}

ただこれはボイラープレートコードが増えますし、なにより折角 Kotlin Android Extensions を入れているのに、負けた気がします…。そこで登場するのが LayoutContainer Support です。

ただし、注意点として、LayoutContainer Support は Experimental Mode となる為、将来的に変更される可能性も十分ある事を考慮しておく必要はあります。(NewsPicksでは、最悪自分が最後責任を持つしという事で採用しています)

実際に使うためには Experimental Mode である為、 まず build.gradle に experimentail を ON にする設定を行ないます。

androidExtensions {
    experimental = true
}

そうする事で、このようにすっきり ViewHolder を記述する事ができます。

private iclass HogeViewHodler(override val containerView: View) 
  : RecyclerView.ViewHolder(containerView), LayoutContainer {

  fun show() {
    // LayoutContainer を実装する事で ViewBinding がここでも使える ! そしてきちんと View のキャッシュもされる! ナイス!
    title.text = "hogehoge"     
  }
}

今回は長くなってしまう為、実際に Kotlin のコードがどういうバイトコードに変換されているかを調べる方法については記載しませんが、実際に Kotlin から生成されるバイトコードをデコンパイルする事で、findViewByid 部分がきちんとキャッシュされるようになっている事が確認できると思います。

最後に

NewsPicks では、 Android を一緒に開発してもらえるメンバーを募集しています。 NewsPicks、 NewsPicks アカデミア 共に大きく成長しているフェーズの為、自分の関わったアプリが多くのユーザー様に使っていただけるという醍醐味を感じる事ができると思っています。

また、特に NewsPicks アプリについてはリリースして時間も経過している為、内部のアーキテクチャについて、より今後の事業の成長を阻害しないものへ改善していく必要があると感じています。 Android のアーキテクチャをこうしていきたい! という意思を持っている方も大歓迎です。

自分の腕を磨きたい! アプリだけでなくサーバサイドも開発していきたい! という方も歓迎しておりますので是非お気軽にご連絡ください 😃

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more