30
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

KotlinでScopeクラスを作ってその中だけで呼べる拡張関数を定義する

Last updated at Posted at 2020-11-04

今日ご紹介させていただくのは、Kotlinの拡張関数を特定の条件下のみで使える様にする。
といった実装の話です。

通常の拡張関数ではできないことが、特定の条件下ではできるようになることが今回のポイントになります。
これによって、ちょっと面倒な処理で、同じコードの繰り返しが多い場合に非常に楽になると思います。
ただし便利な反面設計が重要になります。より発展的なことをしようとすると隠蔽された実装のなかで、特に状態管理やAPI設計が非常に重要になるので、諸刃の剣になると言うことを十分に理解した上で適所のみに使うことが重要です。

ではさっそく段階を踏んで紹介していきましょう。

レベル1

まず最初は特定のクラス内でのみ使える拡張関数からです。

class SquareScope {
  fun Int.square(): Int = this * this
}

上記のようにSquareScopeクラスの中に拡張関数を定義した場合、SquareScopeのインスタンスをthisで参照できる場合のみ拡張関数のsquareを利用することができます。

class SquareScope {
    // SquareScopeがthisの時だけ使える拡張関数
    fun Int.square(): Int = this * this

    fun square_test() {
        println(22.square())
    }
}
fun square_test2() {
    // ちょっと応用、withの中でも使える
    with(SquareScope()) {
        println(22.square())
    }
}
fun square_test3() {
    println(22.square()) // コンパイルエラー
}

レベル2

Kotlinが好きな人であれば、square_test2()でwithを利用している点をみれば、これがどう言う面白さを持っているのか見えてきたかもしれません。
次は特定のラムダ関数引数でのみ使える拡張関数の定義のしかたです。とくにレベル2にするほどのことではないかもしれませんが、レベル1よりは発展的な内容であるので、段階をあえて踏んでいます。

class SquareScope {
    // SquareScopeがthisの時だけ使える拡張関数
    fun Int.square(): Int = this * this
}

fun squareScope(block: SquareScope.()-> Unit) = SquareScope().block()

さて新たにsquareScope()という関数を用意しました。
これを用意することで、先ほどのsquare_test2()で行ったことが次のように実装できます。

fun square_test2_1() {
    // ラムダを引数に使った関数で応用
    squareScope {
      println(22.square())
    }
}

先ほどよりスッキリ書ける様になりました。

レベル3

さていよいよここから紹介したい内容になっていくのですが、これまでは通常の拡張関数が特定の条件下でのみ使える様になると言ったものではありましたが、通常の拡張関数にも置き換え可能な点がここまでの話での限界でした。
次は冒頭でも言った様に、通常の拡張関数では置き換えができないと言う点がポイントになります。

class DateFormatScope(private val format: String) {
    fun Date.simpleFormat(): String {
        return SimpleDateFormat(format, Locale.getDefault()).format(this)
    }
}
fun withFormat(format: String, block: DateFormatScope.() -> Unit) =
    DateFormatScope(format).block()

さて今回はSquareScopeクラスではなくDateFormatScopeとそれを使うwithFormat()という関数を定義しました。
これは以下のようにつかうことができます。

fun withFormat_test() {
    withFormat("yyyy.MM.dd") {
        println(Date().simpleFormat()) // ex. 2020.11.04
    }
    withFormat("yyyy.MM.dd-HH:mm") {
        println(Date().simpleFormat()) // ex. 2020.11.04-11:36
    }
}

このようにDate.simpleFormat()という拡張関数の振る舞いが提供するScopeクラスのインスタンスが変わることで変更されたことになり、通常の拡張関数では考えられない動作をしていることになるとおもいます。

これは特に使っていて便利だから紹介したと言うものではないですが、たとえばたくさん日付の処理を書かなければいけなくて、フォーマットも複数扱わなければいけない時に便利になるかもしれません。
また、たとえばoffsetを考慮した座標計算をしなくてはいけないときに、毎回offsetを差し引きする実装だと読むのが辛い(プログラミング作業のほどんどはコードを読むことだと言う話もあります)というパターンでもつかえるのではないでしょうか?ex. withOffset(x=5, y=20){}

レベル4

さらに応用です。
これも、さほど先ほどとは変わらないのですが、一度にやってしまうと混乱してしまう可能性があるので段階を踏んでいます。
せっかくなので実践的な内容にしていきたいとおもいます。AndroidのComponentActivityでの利用を想定した例です。

val Activity.contentView: View get() = findViewById<ViewGroup>(android.R.id.content)[0]

fun <T : ViewDataBinding> ComponentActivity.requireBinding(): T =
    DataBindingUtil.bind(contentView) ?: error("please setup data binding")

fun  <T : ViewDataBinding> ComponentActivity.bindingScope(block: BindingScope<T>.()->Unit): T = 
    requireBinding<T>().apply { 
        BindingScope(this, this@bindingScope).block() 
    }

class BindingScope<T : ViewDataBinding>(val binding: T, private val activity: ComponentActivity) {
    init {
        binding.lifecycleOwner = activity
    }
    fun Int.spanGridLayoutManager(): GridLayoutManager = GridLayoutManager(activity, this)
    fun RecyclerView.decorationSpace(value: Int) {
        addItemDecoration(SpacesItemDecoration(value))
    }
}

上記のようにDataBindingに関する拡張関数を用意してみました。
どこら変がレベル4なのかというと、ジェネリック関数をつかっているところがメインではあるのですが、追加するならより実践的な内容に踏み込んでいるというところもポイントではあるとおもいます。

今回ご紹介したコードが実際に使うことがあるかどうかは別にしろ、特定のボイラープレートを緩和することも可能です。
以下が実際に使ってみたコードです。
コンストラクタにレイアウトを渡している上でのDataBindingのセットアップをより簡易的に書ける様にしているのがポイントです。
今回の例については、Androidの開発では毎回煩わしい部分ではあるので、複数のテクニックを積み上げてのボイラープレートの削減と利便性の向上がここでは見込めると思います。

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val mainBinding = bindingScope<ActivityMainBinding> {
            binding.list.apply {
                layoutManager = 3.spanGridLayoutManager()
                decorationSpace(4)
            }
        }
    }
}

Activityのリークが怖いと言われる可能性がありますが、bindingScope()の中だけでBindingScopeのインスタンスが消費されていることと、用途的にはbinding.apply{}を使っているのとほとんど変わらない事に注意してください。
またこれは単にこんな感じのことができるという可能性を示すもので、これが良いのでご紹介と言う話ではない事にも注意していただければと思います。

おわりに

さて、いかがだったでしょうか?僕はこう言うことができると知ったのはJetpack Composeに定義されているDrawScopeなどのいくつかのスコープクラスからでした。なのでJetpack Composeで色々やられている方はすでに知っている内容だったかもしれません。
しかし今回初めて知った方はKotlinという言語がどれだけボイラープレート省略にすぐれている言語であるかをある程度実感していただけたのではないかと思いますし、そうであれば本当に嬉しいことです。

ではまた何か、便利になる様なものがあれば紹介させていただきたいと思います。

enjoy!

30
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?