2021年5月14日金曜日

inline class および value class で kotlinx.serialization (JSON) が動く組み合わせ

inline class および value class で kotlinx.serialization (JSON) が動く組み合わせを調べてみた


inline class のときのコード @Serializable inline class ItemId(val value: String) @Serializable data class Item(val id: ItemId, val name: String) fun main() { val item = Item(ItemId("1"), "Android") val json = Json.encodeToString(item) println(json) println(Json.decodeFromString<Item>(json)) } value class のときのコード @Serializable @JvmInline value class ItemId(val value: String) @Serializable data class Item(val id: ItemId, val name: String) fun main() { val item = Item(ItemId("1"), "Android") val json = Json.encodeToString(item) println(json) println(Json.decodeFromString<Item>(json)) }

Kotlin: 14.32, kotlinx.serialization: 1.1.0 + inline class

ビルドエラーになる

e: org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during file facade code generation

Kotlin: 1.4.32, kotlinx.serialization: 1.2.1 + inline class

ビルドエラーになる

e: org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during file facade code generation

Kotlin: 1.4.32, kotlinx.serialization: 1.1.0 + value class

@JvmInline が無いのでビルドエラーになる

Kotlin: 1.4.32, kotlinx.serialization: 1.2.1 + value class

@JvmInline が無いのでビルドエラーになる

Kotlin: 1.5.0, kotlinx.serialization: 1.1.0 + inline class

動く

Kotlin: 1.5.0, kotlinx.serialization: 1.2.1 + inline class

動く

Kotlin: 1.5.0, kotlinx.serialization: 1.1.0 + value class

動く

Kotlin: 1.5.0, kotlinx.serialization: 1.2.1 + inline class

動く


Kotlin を 1.5.0 にすれば kotlinx.serialization を 1.2 にしなくても inline class と value class 両方で動いた。


2021年5月9日日曜日

Jetpack Compose : Canvas Composable を使う

View の onDraw() で描画して Custom View を作る、というのを Compose でやりたいときは Canvas Composable を使います。

Canvas に渡す onDraw lamnda は Receiver が DrawScope になっています。
DrawScope からは描画エリアの大きさとして size: Size が取れます。

また、DrawScope は Density を継承しているので、Dp.toPx() とかも呼び出せます。 @Composable fun CircleProgress( progress: Int, modifier: Modifier, colorProgress: Color = MaterialTheme.colors.primary, colorBackground: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f) .compositeOver(MaterialTheme.colors.surface), strokeWidth: Dp = 8.dp, ) { Canvas(modifier = modifier) { val stroke = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round) val diameter = min(size.width, size.height) - stroke.width val topLeft = Offset((size.width - diameter) / 2, (size.height - diameter) / 2) val circleSize = Size(diameter, diameter) drawArc( color = colorBackground, startAngle = -90f, sweepAngle = 360f, useCenter = false, style = stroke, topLeft = topLeft, size = circleSize, ) drawArc( color = colorProgress, startAngle = -90f, sweepAngle = 360f / 100 * progress, useCenter = false, style = stroke, topLeft = topLeft, size = circleSize, ) } } @Preview @Composable fun CircleProgressPreview() { var progress by remember { mutableStateOf(0) } val animateProgress by animateIntAsState(targetValue = progress, animationSpec = tween()) Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp) ) { Button(onClick = { progress = Random.nextInt(0, 101) }) { Text("Change Progress") } Spacer(modifier = Modifier.height(16.dp)) CircleProgress( progress = animateProgress, modifier = Modifier.size(120.dp) ) } } animate**AsState などでアニメーションも簡単にできます。



Jetpack Compose で Material Design の ToggleButton を作ってみた

Material Design の ToogleButton (https://material.io/components/buttons#toggle-button) を Jetpack Compose で作ってみた。

https://github.com/yanzm/ComposeToggleButton

こんな感じのやつ。


選択の処理は Modifier.toggleable() を使えば OK。 @Composable fun IconToggleButton( imageVector: ImageVector, contentDescription: String?, checked: Boolean, onCheckedChange: (Boolean) -> Unit, enabled: Boolean = true ) { CompositionLocalProvider( LocalContentColor provides contentColor(enabled = enabled, checked = checked), ) { Box( contentAlignment = Alignment.Center, modifier = Modifier .toggleable( value = checked, onValueChange = onCheckedChange, role = Role.RadioButton, ) .size(48.dp) ) { Icon( imageVector = imageVector, contentDescription = contentDescription, modifier = Modifier.size(24.dp) ) } } }

枠線などの描画は Modifier.drawWithContent() でやったが、これが面倒だった〜(特にRTL対応)。 private fun Modifier.drawToggleButtonFrame( ... ): Modifier = this.drawWithContent { ... // draw checked border drawPath( path = ..., color = checkedBorderColor, style = Stroke(strokeWidth), ) drawContent() }

Jetpack Compose : Modifier.triStateToggleable() で3状態ボタンを作る

Modifier.triStateToggleable() を使った3状態チェックボックスとして TriStateCheckbox が用意されています。 var state by remember { mutableStateOf(ToggleableState.On) } TriStateCheckbox(state = state, onClick = { state = when (state) { ToggleableState.On -> ToggleableState.Indeterminate ToggleableState.Indeterminate -> ToggleableState.Off ToggleableState.Off -> ToggleableState.On } })



Modifier.triStateToggleable() を使って独自の3状態ボタンも作ることができます。 var state by remember { mutableStateOf(ToggleableState.On) } Box( contentAlignment = Alignment.Center, modifier = Modifier.triStateToggleable( state = state, onClick = { state = when (state) { ToggleableState.On -> ToggleableState.Off ToggleableState.Off -> ToggleableState.Indeterminate ToggleableState.Indeterminate -> ToggleableState.On } }, role = Role.Checkbox, interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple( bounded = false, radius = 24.dp ) ) ) { Icon( imageVector = when (state) { ToggleableState.On -> Icons.Default.Favorite ToggleableState.Off -> Icons.Default.FavoriteBorder ToggleableState.Indeterminate -> Icons.Default.FavoriteBorder }, contentDescription = when (state) { ToggleableState.On -> "favorite on" ToggleableState.Off -> "favorite on" ToggleableState.Indeterminate -> "favorite indeterminate" }, tint = when (state) { ToggleableState.On -> MaterialTheme.colors.primary ToggleableState.Off -> MaterialTheme.colors.primary ToggleableState.Indeterminate -> MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled) } ) }



Jetpack Compose : Modifier.toggleable() で独自チェックボックスを作る

Modifier.toggleable() を使います。 var checked by remember { mutableStateOf(false) } Box( contentAlignment = Alignment.Center, modifier = Modifier .toggleable( value = checked, onValueChange = { checked = it } ) .size(48.dp) ) { Icon( imageVector = if (checked) Icons.Default.Favorite else Icons.Default.FavoriteBorder, contentDescription = if (checked) "favorite on" else "favorite off", ) } Box + Modifier.toggleable() 部分は IconToggleButton として用意されているので、それを使うこともできます。 var checked by remember { mutableStateOf(false) } IconToggleButton( checked = checked, onCheckedChange = { checked = it }, ) { Icon( imageVector = if (checked) Icons.Default.Favorite else Icons.Default.FavoriteBorder, contentDescription = if (checked) "favorite on" else "favorite off", ) }



2021年5月2日日曜日

LazyColumn (LazyRow) の item 指定は index で頑張らなくていい

とある発表資料で見かけたのですが、LazyColumn (LazyRow)ではこういう index で頑張る方法は必要ありません。
RecyclerView がこういう頑張りをしないといけなかったので、こうやってしまう気持ちはわかります。

よくない例 @Composable fun DogList(list: List<Dog>) { LazyColumn { items(list.size + 1) { if (it == 0) { Header() } else { DogListItem(list[it - 1]) } } } }

どうするのが良いかというと、素直に Header() と list で item/items を分ければいいんです。items() には数字ではなく List<T> をとる拡張関数が用意されています。

よい例 @Composable fun DogList(list: List<Dog>) { LazyColumn { item { Header() } items(list) { dog -> DogListItem(dog) } } } また、itemsIndexed() を使うと index もとれるので、例えば dog.name の1文字目が変わったら区切りヘッダーを入れるというのもこんな感じで簡単に書けます(list は dog.name で sort されている前提)。 @Composable fun DogList(list: List<Dog>) { LazyColumn { itemsIndexed(list) { index, dog -> if (index == 0 || list[index - 1].name[0] != dog.name[0]) { NameDivider(dog.name[0]) } DogListItem(dog) } } }



2021年5月1日土曜日

Jetpack Compose : 角丸をパーセントで指定する

RoundedCornerShape() には Int または Float でパーセントを指定することができます。

RoundedCornerShape(50) // 50dp ではなく、50% ということ

@Composable fun Capsule() { Text( text = "Android", modifier = Modifier .padding(16.dp) .background( color = Color.LightGray, shape = RoundedCornerShape(50) ) .padding(vertical = 8.dp, horizontal = 16.dp) ) }