KotlinでFragmentのargumentsをいい感じに読み取りたい
夢と魔法の待ち時間というAndroidアプリを趣味で作っていて、これを100% Kotlinに移行するためここ最近ごりごりコードを書います。
1つのフラグメントで両パーク(陸と海)に対応するようにしていて、そんな感じのフラグメントがいくつかあります。
例えばアトラクション一覧のフラグメントとか、グリーティング一覧のフラグメントとか。
どちらのパークかはarguments
で与えられるという寸法です。
class AttractionFragment: Fragment() { val park: Park get() = arguments.getSerializable("park") as Park // 省略 }
class GreetingFragment: Fragment() { val park: Park get() = arguments.getSerializable("park") as Park // 省略 }
だいたいこんな感じでpark
の部分が完全に一致。
すっきり記述できる方法はないかなーと考えました(BaseFragment
とかの導入は却下)。
ScalaのSelf Typeみたいなやつ (失敗)
失敗というか既に廃止された言語機能です。
次のコードのように、インタフェースが自分の実装されるクラスを指定できる感じです。
WithPark
インタフェースはFragment
を要求しているので、自分の中でFragment
のarguments
にアクセスできます。
interface WithPark: Fragment { val park: Park get() = arguments.getSerializable("park") as Park } class AttractionFragment: Fragment(), WithPark { fun とあるメソッド() { println(park) // パークをargumentsから取れる } }
インタフェースだけで頑張る (成功)
話の順序的に真ん中に成功例を載せます。
arguments
を抽象プロパティとして提供するインタフェースにすれば、それっぽくなります。
interface WithPark { fun getArguments(): Bundle val park: Park get() = getArguments().getSerializable("park") as Park } class AttractionFragment: Fragment(), WithPark { fun とあるメソッド() { println(park) // パークをargumentsから取れる } }
問題があるとすればFragment
以外がWithPark
になれる点です。
いや、こっちの方がより抽象的でいいのかな。でもスコープというか使用できる範囲は狭いほうがいいような。。
拡張プロパティで頑張る (失敗)
最後にまた失敗例です。
今回はWithPark
インタフェースは、拡張プロパティのための印です。
interface WithPark val <T> T.park: String where T: Fragment, T: WithPark get() = argument.getString("park") as Park class AttractionFragment: Fragment(), WithPark { fun とあるメソッド() { println(park) // なぜかここでコンパイルエラー } }
拡張プロパティpark
は、Fragment
かつWithPark
な型をレシーバに取ります。
これにより最初に挙げた例のSelf Typeのようなことができます。
WithPark
は誰でも実装できますが、それがFragment
以外だとpark
プロパティを呼び出せません。
しかし何故か呼び出し箇所でコンパイルエラーになってしまいます。 数日前に同じ問題に直面した人がチケット上げてました。
https://youtrack.jetbrains.com/issue/KT-9630
(僕の中での)結論
3番目の方法ができるのを待って、それまでは2番目の方法で凌ぐ。
第2回関西Kotlin勉強会で発表してきたよ〜 #ashiyakt
Kotlin M13で追加されたsealed class
Kotlin マイルストーン13 がリリースされました!
lateinit
修飾子については↓の記事をば。
Kotlin M13で追加されたlateinit試してみた - 算譜王におれはなる!!!!
sealed class
クラスに付けられるsealed
修飾子が追加されました。
一言で言うと、クラスの継承を制限するための修飾子です。
sealed
が付いたクラスを継承するにはある条件を満たす必要があるということです。
そういう意味でScalaのsealed
と似ていてC#のそれとは異なります。
肝心のsealed classを継承するための条件ですが、現時点ではsealed classにネストされたクラスであることです。
sealed class A { class B: A() } class C: A() // これはダメ
そのうち、この制限を緩和して同一ソースファイル内での継承も許可するそうです。
さて、このsealed classですが何の役に立つのかと言うと、リリースノート内でAlgebraic Data Types(代数的データ型)というキーワードが登場しています。 例えば列挙型をenum classではなくsealed classとそのサブクラスで表現可能です。
sealed class Color { object Red: Color() object Green: Color() object Blue: Color() } val name = when(color) { Color.Red -> "赤" Color.Green -> "緑" Color.Blue -> "青" }
when式の分岐でelse
がないことに注目してください。
Color
はsealed class
として定義されており、他の場所でそのサブクラスが定義されないことを保証します。
そのためwhen式の分岐でRed
、Green
、Blue
すべての場合が考慮されていることをコンパイラは知っているのでelse
が不要になります。
Color
がopen class
として定義された場合には、他の場所でもそのサブクラスが定義される可能性があるのでコンパイラはwhen式にelse
を要求します。
次に、sealed classを使って直和型を表現してみます。
import Option.* sealed class Option<out T> { class Some<T>(val value: T): Option<T>() object None: Option<Nothing>() } val name = when (userOption) { is Some<User> -> userOption.value.name is None -> "null" }
便利ですね〜。
ちなみにインタフェースにはsealed
を付けられないみたいです。
Kotlin M13で追加されたlateinit試してみた
Kotlin マイルストーン13 がリリースされました!
その中で追加されたlateinit
というプロパティにつける修飾子が便利そう。
プロパティの初期化を先延ばしにできるので既存のフレームワークにインジェクトしてもらえるって寸法です。
「プロパティの初期化の先延ばし」っていうとDelegated propertyを思い出すけど、これには問題があったのでした。。
lateinitってDelegates.nonNull()でも同じことできるんじゃないの
— たろう (@ngsw_taro) 2015, 9月 16
@ngsw_taro バイトコード見ると、「private final Lkotlin/properties/ReadWriteProperty; _hello$delegate」ってなってるので、型がちがくてインジェクションしてもらえなくて(´・ω・`)
— しおしお (@_siosio_) December 3, 2014
ということでlateinit
を試してみました。
import com.google.inject.Guice import javax.inject.Inject import kotlin.properties.Delegates class Greeter { fun greet() { println("Hello") } } class Client { @Inject lateinit val greeter1: Greeter @Inject val greeter2: Greeter by Delegates.notNull() } fun main(args: Array<String>) { val injector = Guice.createInjector() val client = injector.getInstance(Client::class.java) client.greeter1.greet() //=> Hello client.greeter2.greet() // IllegalStateException thrown }
Google Guiceでインジェクトしています。
lateinit
が付いているgreeter1
は見事インジェクトされています!しかし、Delegates.notNull()
をつけているgreeter2
はインジェクトされておらずgreet()
呼び出し時に初期化が済んでないよと例外が投げられてしまいました。
そのほかの変更点についてはまた後ほど。
ちなみにこれならいける
var greeter2: Greeter by Delegates.notNull() @Inject set