Upgrade to Pro — share decks privately, control downloads, hide ads and more …

原則から考える保守しやすいComposable関数設計

 原則から考える保守しやすいComposable関数設計

コードの保守性を高めるための原則や手法は数多く提唱されています。
このセッションでは、それらの中でも「関心の分離」と「凝集度」に着目し、メンテナンスしやすいComposable関数の設計について考えます。

DroidKaigi.collect { #19@Osaka }
https://droidkaigi.connpass.com/event/356733/

Avatar for Mori Atsushi

Mori Atsushi

June 20, 2025
Tweet

More Decks by Mori Atsushi

Other Decks in Technology

Transcript

  1. o ff setの保持 o ff setの設定 @Composable fun DraggableScreen() {

    val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } val coroutineScope = rememberCoroutineScope() Box(modifier = Modifier.fillMaxSize()) { Box( Modifier .offset { offset.value.round() } .background(Color.Blue) .size(60.dp) .clickable { coroutineScope.launch { offset.animateTo(Offset(0f, 0f)) } } // … クリック時に初期位置にリセット 改善前
  2. // … .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume()

    coroutineScope.launch { offset.snapTo( Offset( x = offset.value.x + dragAmount.x, y = offset.value.y + dragAmount.y, ) ) } } } ) } } drag時の処理 改善前
  3. class DraggableState { private val animatable = Animatable(INITIAL_OFFSET, Offset.VectorConverter) val

    offset: IntOffset get() = animatable.value.round() suspend fun drag(dragAmount: Offset) { animatable.snapTo( Offset( x = animatable.value.x + dragAmount.x, y = animatable.value.y + dragAmount.y, ) ) } suspend fun reset() { animatable.animateTo(INITIAL_OFFSET) } companion object { private val INITIAL_OFFSET = Offset(0f, 0f) } } drag時の処理 Click時の処理 改善後
  4. drag時の処理 Click時の処理 改善後 @Composable fun DraggableScreen() { val state =

    remember { DraggableState() } val coroutineScope = rememberCoroutineScope() Box(modifier = Modifier.fillMaxSize()) { Box( Modifier .offset { state.offset } .background(Color.Blue) .size(60.dp) .clickable { coroutineScope.launch { state.reset() } } .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() coroutineScope.launch { state.drag(dragAmount) } } }, ) } }
  5. 論理的凝集 3.4 凝集度を高める fun output(text: String, target: Target) { when

    (target) { Target.Email -> /* ϝʔϧͷૹ৴ॲཧ */ Target.Log -> / * ϩάͷग़ྗॲཧ * / } } enum class Target { Email, Log } コードを書く上でその機能や目的ではなく、 論理的に関連しているものを集めた状態を指します。 l メール送信とロ グ 出力は本質的に異なる。 共通するのは「テキストを出力する」という点のみ
  6. アイコンの場合 改善前 @Composable fun ContentWithBackground(content: Content, modifier: Modifier = Modifier)

    { Box( modifier = modifier .background(Color(0xFFBBDEFB), RoundedCornerShape(8.dp)) .padding(16.dp) ) { when (content) { is Content.Icon -> Icon(imageVector = content.imageVector, contentDescription = null) is Content.Text -> Text(text = content.text, fontSize = 16.sp) } } } sealed interface Content { data class Icon(val imageVector: ImageVector) : Content data class Text(val text: String) : Content } テキストの場合
  7. 改善後 @Composable fun RounededBlueBackground(modifier: Modifier = Modifier, content: @Composable ()

    -> Unit) { Box( modifier = modifier .background(Color(0xFFBBDEFB), RoundedCornerShape(8.dp)) .padding(16.dp) ) { content() } } @Composable fun IconContent(imageVector: ImageVector, modifier: Modifier = Modifier) { Icon(modifier = modifier, imageVector = imageVector, contentDescription = null) } @Composable fun TextContent(text: String, modifier: Modifier) { Text(modifier = modifier, text = text, fontSize = 16.sp) } スロットパターン
  8. 改善後 (別のアイ デ ィア) @Composable fun IconWithBackground(imageVector: ImageVector, modifier: Modifier

    = Modifier) { Icon( modifier = modifier.roundedBlueBackground(), imageVector = imageVector, contentDescription = null, ) } @Composable private fun TextWithBackground(text: String, modifier: Modifier) { Text( modifier = modifier.roundedBlueBackground(), text = text, fontSize = 16.sp, ) } private fun Modifier.roundedBlueBackground(): Modifier = this .background(Color(0xFFBBDEFB), RoundedCornerShape(8.dp)) .padding(16.dp) 共通背景を定義
  9. 時間的凝集 3.4 凝集度を高める fun initApp() { initConfig() initLogger() initDatabase() }

    同じタイミン グ に動作するものを集めた状態を指します。 (中略)中身の実行順序を入れ替えても動作するという特徴があります。 l アプリ起動時に行われる処理を集めた関数
  10. 手続き的凝集 3.4 凝集度を高める fun outputFile(file: File) { checkPermission() writeFile(file) }

    処理の順序に意味がある操作を1つにまとめた状態を指します。 先ほどの時間的凝集とは異なり、順序を入れ替えると 正しく動作しなくなるという特徴があります。 l ファイルを書き込む前に権限をチェックする必要がある
  11. 改善前 @Composable fun ShadowButton( label: String, onClick: () -> Unit,

    modifier: Modifier = Modifier, ) { val shape = RoundedCornerShape(8.dp) Text( modifier = modifier .shadow(elevation = 3.dp, shape = shape) .background(Color(0xFFBBDEFB), shape) .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = 12.dp), text = label, ) }
  12. 改善前 @Composable fun ShadowButton( label: String, onClick: () -> Unit,

    modifier: Modifier = Modifier, ) { Text( modifier = modifier.shadowButton(onClick = onClick), text = label, ) } private fun Modifier.shadowButton(onClick: () -> Unit): Modifier { val shape = RoundedCornerShape(8.dp) return this .shadow(elevation = 3.dp, shape = shape) .background(Color(0xFFBBDEFB), shape) .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = 12.dp), } ボ タン専用Modi fi er
  13. @Composable fun ShadowButton( label: String, onClick: () -> Unit, modifier:

    Modifier = Modifier, ) { Text( modifier = modifier .roundedBackgroundWithShadow() .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = 12.dp), text = label, ) } private fun Modifier.roundedBackgroundWithShadow(): Modifier { val shape = RoundedCornerShape(8.dp) return this .shadow(elevation = 3.dp, shape = shape) .background(Color(0xFFBBDEFB), shape) } 背景のみのModi fi er 改善後