読者です 読者をやめる 読者になる 読者になる

visible true

技術的なメモを書く

jetBrains/anko触ってたらよくわからない実装を見たので調べたらKotlinで引数に拡張関数を取る関数を作れる事がわかった

Kotlin anko

JetBrains/ankoというイカしたUI構築用DSLが出ましたね。ankoの良さについてはまだ語れるほど使い込んでないので今度にするとして、ankoの実装見てたらよくわからない書き方に出会ったので調べましたメモです。

github.com

ankoとは

ankoはKotlinで書かれたUI構築用のDSLで、Androidのレイアウト構築をいい感じに助けるものです。具体的には拡張関数によってレイアウト作成周りのメソッドをActivity等に生やし、ゴリゴリコード書くとUIを定義しつつButtonのevent処理もついでに書けて再利用が可能でコンパイル時点で型チェックするので腐らないし*1、layout xmlを経由しないので高速でサイコーだよみたいなものです。kotlin projectでしかまともには使えないような気がするので導入ハードルは高めな印象です。

ankoのソースを読む

早速試そうって事で公式のサンプルプロジェクトを動かしつつイジりました。典型的なコードとしては以下です。

class MainActivity : Activity() {
  fun setupUi() {
    verticalLayout {
      padding = dip(32)

      imageView(android.R.drawable.ic_menu_manage).layoutParams {
        margin = dip(16)
        gravity = Gravity.CENTER
      }

      val name = editText {
        hintResource = R.string.name
      }
      val password = editText {
        hintResource = R.string.password
        inputType = TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_PASSWORD
      }
      button("Log in") {
        onClick {
          tryLogin(name.text, password.text)
        }
      }
    }
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setupUi()
  }
}

例えばverticalLayout{}はActivityに拡張関数によって追加されたメソッドです。ただのメソッドなのでcommand+B(Go To Declaration)などで実装を辿っていけます。

意味不明な関数

verticalLayout{}の定義に飛んでみると、以下の様な実装が。

public fun Activity.verticalLayout(init: _LinearLayout.() -> Unit): LinearLayout =
        __dslAddView(verticalLayoutFactory, init, this): LinearLayout

割と意味不明ですが、fun Activity.verticalLayoutはActivityにverticalLayoutという名前のメソッドを生やすという拡張関数の構文です。(init: _LinearLayout.() -> Unit)は引数を表し、: LinearLayoutは戻り値を表します。

ところで引数initの_LinearLayout.() -> Unitがよくわからないですね。関数っぽいんですが関数を引数に取る場合通常は以下の様に書きます。

fun calc(function: (Int) -> Int): Int {
  return function.invoke(10)
}

(Int) -> IntIntを引数にとってIntを返す関数を表します。ankoの実装では_LinearLayout.() -> Unitって感じで_LinearLayoutがなんか余計ですね。

拡張関数を引数に取る関数

色々調べたり触ったりしているとどうもこれは拡張関数を引数に取る場合の書き方だという事がわかりました。その引数が生きているスコープでのみ拡張対象のクラスに作用する拡張関数という事のようです。実装例としては以下の様になります。

public class Recipe(var name: String) {
  fun bindName(activity: Activity, finder: Activity.() -> TextView?): Unit {
    activity.finder()?.setText(name)
  }
  fun finder(): Activity.() -> TextView? {
    return {
      findViewById(R.id.sample_text) as TextView?
    }
  }
}

RecipeクラスのbindName()でActivityと拡張関数を貰う様にしてます。それをactivity.finder()という風に呼び出し、TextViewを受け取りnameをセットしています。また、finder()Activity.() -> TextView?を返す関数で、その関数の内部はActivityのスコープになっています。その為findViewByIdを呼び出せます。Activity側では以下の様に使います。

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)
  val recipe = Recipe("おいしいたこ焼き");
  recipe.bindName(this, recipe.finder())
}

上記コードは動作確認の為なので実用性はないです。ただこの仕組みを使っていかしたライブラリが作れる可能性はある気がします。そう、ankoのように。

おわりに

RxJavaとretrolambdaサイコーではと思ってたけどkotlinもlambdaあるしRxJava+kotlinでいいかもなって思っていたなかでanko来た感じなのでマジGoogle I/O 2015でKotlinサポート来るんじゃないかみたいな感覚がある。来なかったら辛いので実用はI/Oまで待つ。

*1: (TextView)findViewById()みたいなダサくて危険な事しなくてよい