2018年11月10日土曜日

sealed class に共通データを持たせるときは abstract val にする

sealed class Pet を継承した data class Cat と Dog があるとします。 sealed class Pet data class Cat(val name: String, val kind: CatKind) : Pet() data class Dog(val name: String, val kind: DogKind) : Pet() name プロパティは Pet として必須なので Pet class に持たせたいですね。
しかし次のように primary コンストラクタの property paramter として持たせようとするとコンパイルエラーになります。 sealed class Pet(val name: String) data class Cat(name: String, val kind: CatKind) : Pet(name) // コンパイルエラー data class Dog(name: String, val kind: DogKind) : Pet(name) // コンパイルエラー data class の primary コンストラクタは property parameter (val or var) しか持てないからです。 そこで、どうするかというと name を abstract val として Pet に持たせます。 sealed class Pet { abstract val name: String } data class Cat(override val name: String, val kind: CatKind) : Pet() data class Dog(override val name: String, val kind: DogKind) : Pet()


2018年11月4日日曜日

古い Mockito では Kotlin の suspend fun を override してくれないので 2.23.0 以降を使う

追記

mockito 2.23.0 で suspend fun のサポートが入った(Support mocking kotlin suspend functions compiled by Kotlin 1.3 (#1500))と教えていただいたので、試してみました。

2.23.0 の mockito なら以下のテストが成功しました!やったー! // このテストが 2.23.0 の mockito なら成功する! @Test fun test() { runBlocking { val mock = mock(Greeting::class.java).apply { `when`(hello()).thenReturn("Hello Android") } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } もともと試した mockito のバージョンは 2.8.9 です。(なぜこのバージョンかというと iosched がこのバージョンを使っていたからです。特に深い意味はありません。)


以下は mockito 2.8.9 での動作です。

以下のような interface と class があるとします。 interface Greeting { fun hello(): String } class GreetingTextCounter(private val greeting: Greeting) { fun count(): Int { return greeting.hello().length } } Greeting のモックを用意して特定の文字列を hello() で返すようにすれば、GreetingTextCounter.count() のテストができます。 @Test fun test() { val mock = mock(Greeting::class.java).apply { `when`(hello()).thenReturn("Hello Android") } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } ここで Greeting.hello() と GreetingTextCounter.count() を suspend にします。
interface Greeting { suspend fun hello(): String } class GreetingTextCounter(private val greeting: Greeting) { suspend fun count(): Int { return greeting.hello().length } } すると、以下のテストは失敗します。 // このテストは失敗する @Test fun test() { runBlocking { val mock = mock(Greeting::class.java).apply { `when`(hello()).thenReturn("Hello Android") } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } `when`().thenReturn() で hello() のときに "Hello Android" を返すように指定していても、GreetingTextCounter.count() のところで greeting.hello() が null を返してしまい、 java.lang.NullPointerException になります。

以下のように Greeting を実装した object を用意して hello() を override すればテストは成功します。 // このテストは成功する @Test fun test() { runBlocking { val mock = object : Greeting { override suspend fun hello() = "Hello Android" } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } しかし使用しないメソッド以外も override しなければいけないのでよくありません。

そこで Delegation を活用し、使用しないメソッドは Mockito の mock に流すようにします。 // このテストは成功する @Test fun test() { runBlocking { val mock = object : Greeting by mock(Greeting::class.java) { override suspend fun hello() = "Hello Android" } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } これで必要なメソッドだけ override し、テストも通るようになります。


2018年10月26日金曜日

FlexboxLayoutManager では CompoundDrawable の指定に relative 系の属性、メソッドは使わないほうがよい

compileSdkVersion 28, 27 で試しています(将来のリリースで修正されている可能性があります)。

FlexboxLayoutManager の問題ではなく、TextView の measure() 実装の問題です(Issue Tracker に登録しました)。
どういう問題かというと、setCompoundDrawablesRelativeWithIntrinsicBounds()setCompoundDrawablesWithIntrinsicBounds() で TextView の measure() の結果が異なり、setCompoundDrawablesRelativeWithIntrinsicBounds() だと measuredWidth/Height に CompoundDrawables のサイズが含まれないという問題です。 再現コードなどは上記の Issue に書いてあります。

FlexboxLayoutManager は MeasuredWidth/Height の値を使って View を配置しているため、TextView で setCompoundDrawablesRelativeWithIntrinsicBounds() や android:drawableStart, android:drawableEnd などで CompoundDrawable を指定すると、それを含まないサイズで配置され、文字が切れたり折り返して表示されてしまいます。



以下は RecyclerView + FlexboxLayoutManager の例です。



この例では、2行目は android:drawableLeft, android:drawableRight、4行目は android:drawableStart, android:drawableEnd を使っています。それ以外は同じです。セットされているテキストも両方 "Hello Android" です。

2行目用のレイアウト <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawableLeft="@drawable/square" android:drawableRight="@drawable/square" /> 4行目用のレイアウト <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawableEnd="@drawable/square" android:drawableStart="@drawable/square" /> android:drawableStart, android:drawableEnd を使った4行目では、CompoundDrawable のサイズが measuredWidth/Height に反映されず、その分文字の領域が小さくなって "Hello Android" の Android が切れたり、square の下のほうが切れたりしています。

このように CompoundDrawable を relative 系の属性、メソッドで指定すると問題があるため、setCompoundDrawablesWithIntrinsicBounds() や android:drawableLeft, android:drawableRight を使いましょう。