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

Composeのイベント地獄から脱出しよう! The Elm Architectureの導入に...

Avatar for wcaokaze wcaokaze
September 24, 2025

Composeのイベント地獄から脱出しよう! The Elm Architectureの導入にみる可能性

2025/9/24 After DroidKaigi 2025 from ピクシブ

↓ 配信アーカイブはこちら!!
https://youtu.be/P5h4isMYmMY

-------------------------------------------

# Composeのイベント地獄から脱出しよう!The Elm Architectureの導入にみる可能性

Composeでは、複数のUIパーツから成るコンポーネントを関数として手軽にコンポーネント化できます。
しかしその反面、UIコンポーネントから発生するイベント(タップ, 文字の入力, etc)を引数として宣言する必要があり、複雑な画面ではComposableの引数の数は爆発的に増加します。特に巨大な画面ではイベントの数がプログラマには把握するのが困難なほどに膨れ上がり、Composableの引数に大量のイベントが宣言される「イベント地獄」に陥ります。
このセッションでは、「イベント地獄」を緩和するために、The Elm Architectureの思想をComposeに持ち込んで試行錯誤した過程を紹介します。
具体的には、Elm ArchitectureのMsg, Updateの考え方をComposeでどのように実現するのか、導入の結果どんなメリット・デメリットがあったのか、実際のプロダクトに適用した際の内容を交えてお伝えします。
なお、最終的にElm Architectureは採用しないという判断に至りましたが、その過程で見えてきた知見や他のMVI実装への応用のポテンシャルなど、Composeを利用した開発全般にも活かせるような内容です。

Avatar for wcaokaze

wcaokaze

September 24, 2025
Tweet

More Decks by wcaokaze

Other Decks in Programming

Transcript

  1. @Composable internal fun ProposalScreen( uiState: ProposalUiState, windowInsets: WindowInsets, onAppBarBackButtonClick: ()

    -> Unit, onAppBarShareButtonClick: () -> Unit, onBottomBarPrevProposalButtonClick: () -> Unit, onBottomBarNextProposalButtonClick: () -> Unit, onBottomBarLikeButtonClick: () -> Unit, onPullToRefresh: () -> Unit, onErrorRetryButtonClick: () -> Unit, onShareButtonClick: () -> Unit, onLikeButtonClick: () -> Unit, onSpeakerExpandButtonClick: () -> Unit, onSpeakerUrlClick: (Uri) -> Unit, onSpeakerHistoryItemClick: (Uri) -> Unit, ) { イベント地獄、書いてますか?
  2. @Composable internal fun ProposalScreen( uiState: ProposalUiState, windowInsets: WindowInsets, onAppBarBackButtonClick: ()

    -> Unit, onAppBarShareButtonClick: () -> Unit, onBottomBarPrevProposalButtonClick: () -> Unit, onBottomBarNextProposalButtonClick: () -> Unit, onBottomBarLikeButtonClick: () -> Unit, onPullToRefresh: () -> Unit, onErrorRetryButtonClick: () -> Unit, onShareButtonClick: () -> Unit, onLikeButtonClick: () -> Unit, onSpeakerExpandButtonClick: () -> Unit, onSpeakerUrlClick: (Uri) -> Unit, onSpeakerHistoryItemClick: (Uri) -> Unit, ) Kotlin
  3. interface ProposalScreenEvents { fun onAppBarBackButtonClick() fun onAppBarShareButtonClick() fun onBottomBarPrevProposalButtonClick() fun

    onBottomBarNextProposalButtonClick() fun onBottomBarLikeButtonClick() fun onPullToRefresh() fun onErrorRetryButtonClick() fun onShareButtonClick() fun onLikeButtonClick() fun onSpeakerExpandButtonClick() fun onSpeakerUrlClick(url: Uri) fun onSpeakerHistoryItemClick(itemUrl: Uri) } @Composable internal fun ProposalScreen( uiState: ProposalUiState, events: ProposalScreenEvents, ) Kotlin
  4. interface ProposalScreenEvents { fun onAppBarBackButtonClick() fun onAppBarShareButtonClick() fun onBottomBarPrevProposalButtonClick() fun

    onBottomBarNextProposalButtonClick() fun onBottomBarLikeButtonClick() fun onPullToRefresh() fun onErrorRetryButtonClick() fun onShareButtonClick() fun onLikeButtonClick() fun onSpeakerExpandButtonClick() fun onSpeakerUrlClick(url: Uri) fun onSpeakerHistoryItemClick(itemUrl: Uri) } @Composable internal fun ProposalScreen( uiState: ProposalUiState, events: ProposalScreenEvents, ) Kotlin
  5. @Composable internal fun ProposalScreen( uiState: ProposalUiState, onLikeButtonClick: () -> Unit,

    onGoButtonClick: () -> Unit, onSwitchCheckedChange: (Boolean) -> Unit, ) { ... } Kotlin
  6. @Composable internal fun ProposalScreen( uiState: ProposalUiState, onLikeButtonClick: () -> Unit,

    onGoButtonClick: () -> Unit, onSwitchCheckedChange: (Boolean) -> Unit, onCheckboxCheckedChange: (Boolean) -> Unit, ) { ... } Kotlin
  7. Scaffold( topBar = { @OptIn(ExperimentalMaterial3Api::class) TopAppBar( title = { },

    navigationIcon = { IconButton( onClick = onAppBarBackButtonClick, ) { Icon( imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back", ) } }, actions = { IconButton( onClick = onShareButtonClick, ) { Icon( imageVector = Icons.Default.Share, contentDescription = "Share", ) } }, windowInsets = windowInsets, ) }, bottomBar = { BottomAppBar( contentPadding = PaddingValues(horizontal = 16.dp), windowInsets = windowInsets, ) { BottomBarButton( icon = Icons.AutoMirrored.Default.KeyboardArrowLeft, text = "前へ", onClick = onBottomBarPrevProposalButtonClick, ) BottomBarButton( icon = Icons.AutoMirrored.Default.KeyboardArrowRight, text = "次へ", onClick = onBottomBarNextProposalButtonClick, ) Spacer( modifier = Modifier .weight(1.0f) ) BottomBarButton( icon = Icons.Default.FavoriteBorder, text = "いいね", onClick = onBottomBarLikeButtonClick, ) } }, contentWindowInsets = windowInsets, ) { innerPadding -> LoadingUi( state = uiState.proposal, error = { _, _ -> ErrorUi( onRetryButtonClick = onErrorRetryButtonClick, ) } ) { proposal, isReloading -> @OptIn(ExperimentalMaterial3Api::class) PullToRefreshBox( isRefreshing = isReloading, onRefresh = onPullToRefresh, modifier = Modifier .padding(innerPadding) .fillMaxSize() ) { Column( modifier = Modifier .background(MaterialTheme.colorScheme.background) .verticalScroll(rememberScrollState()) .padding(16.dp) ) { Column( modifier = Modifier .fillMaxWidth() ) { Text( text = proposal.title, style = MaterialTheme.typography.headlineMedium ) val formattedDate = remember(proposal.date) { Kotlin
  8. Scaffold( topBar = { @OptIn(ExperimentalMaterial3Api::class) TopAppBar( title = { },

    navigationIcon = { IconButton( onClick = onAppBarBackButtonClick, ) { Icon( imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back", ) } }, actions = { IconButton( onClick = onShareButtonClick, ) { Icon( imageVector = Icons.Default.Share, contentDescription = "Share", ) } }, windowInsets = windowInsets, ) }, bottomBar = { BottomAppBar( contentPadding = PaddingValues(horizontal = 16.dp), windowInsets = windowInsets, ) { BottomBarButton( icon = Icons.AutoMirrored.Default.KeyboardArrowLeft, text = "前へ", onClick = onBottomBarPrevProposalButtonClick, ) BottomBarButton( icon = Icons.AutoMirrored.Default.KeyboardArrowRight, text = "次へ", onClick = onBottomBarNextProposalButtonClick, ) Spacer( modifier = Modifier .weight(1.0f) ) BottomBarButton( icon = Icons.Default.FavoriteBorder, text = "いいね", onClick = onBottomBarLikeButtonClick, ) } }, contentWindowInsets = windowInsets, ) { innerPadding -> LoadingUi( state = uiState.proposal, error = { _, _ -> ErrorUi( onRetryButtonClick = onErrorRetryButtonClick, ) } ) { proposal, isReloading -> @OptIn(ExperimentalMaterial3Api::class) PullToRefreshBox( isRefreshing = isReloading, onRefresh = onPullToRefresh, modifier = Modifier .padding(innerPadding) .fillMaxSize() ) { Column( modifier = Modifier .background(MaterialTheme.colorScheme.background) .verticalScroll(rememberScrollState()) .padding(16.dp) ) { Column( modifier = Modifier .fillMaxWidth() ) { Text( text = proposal.title, style = MaterialTheme.typography.headlineMedium ) val formattedDate = remember(proposal.date) { Kotlin
  9. @Composable internal fun Speaker( speaker: Proposal.Speaker, modifier: Modifier = Modifier,

    ) { Column { Text("登壇者プロフィール") Column { Row { AsyncImage(speaker.avatarUrl) Text(speaker.name) } Text(speaker.description) if (speaker.url != null) { Kotlin
  10. @Composable private fun SpeakerHistory( history: Proposal.Speaker.History, modifier: Modifier = Modifier,

    ) { Column { for (item in history.items) { Row { Text(item.date) Text(history.title) } } } } Kotlin
  11. @Composable internal fun Speaker( speaker: Proposal.Speaker, modifier: Modifier = Modifier,

    ) { ... SpeakerHistory( history = speaker.history ) ... } Kotlin
  12. @Composable internal fun Speaker( speaker: Proposal.Speaker, modifier: Modifier = Modifier,

    ) { ... SpeakerHistory( history = speaker.history ) ... } Kotlin
  13. @Composable fun ProposalScreen() { Speaker( speaker = uiState.proposal.speaker ) }

    @Composable fun Speaker() { SpeakerHistory( history = speaker.history ) } @Composable fun SpeakerHistory() { } Kotlin
  14. @Composable fun ProposalScreen() { Speaker( speaker = uiState.proposal.speaker ) }

    @Composable fun Speaker() { SpeakerHistory( history = speaker.history ) } @Composable fun SpeakerHistory() { } Kotlin
  15. @Composable fun ProposalScreen() { Speaker( speaker = uiState.proposal.speaker ) }

    @Composable fun Speaker() { SpeakerHistory( history = speaker.history ) } @Composable fun SpeakerHistory( onItemClick: (Uri) -> Unit, ) { Kotlin
  16. @Composable fun ProposalScreen() { Speaker( speaker = uiState.proposal.speaker ) }

    @Composable fun Speaker() { SpeakerHistory( history = speaker.history, onItemClick = { uri -> navController.navigate() }, ) } Kotlin
  17. @Composable fun ProposalScreen() { Speaker( speaker = uiState.proposal.speaker ) }

    @Composable fun Speaker( onHistoryItemClick: (Uri) -> Unit, ) { SpeakerHistory( history = speaker.history, onItemClick = onHistoryItemClick, ) } Kotlin
  18. @Composable fun ProposalScreen( onSpeakerHistoryItemClick: (Uri) -> Unit, ) { Speaker(

    speaker = uiState.proposal.speaker, onHistoryItemClick = onSpeakerHistoryItemClick, ) } @Composable fun Speaker( onHistoryItemClick: (Uri) -> Unit, ) Kotlin
  19. ProposalScreen( uiState = uiState, onSpeakerHistoryItemClick = { uri -> navController.navigate(...)

    }, ) @Composable fun ProposalScreen( onSpeakerHistoryItemClick: (Uri) -> Unit, ) Kotlin
  20. ProposalScreen AppBar Proposal Speaker SpeakerHistory ErrorUi LikeButton ShareButton TableOfContents BottomBar

    PullToRefreshBox ShareButton LikeButton NextProposalButton PrevProposalButton
  21. ProposalScreen AppBar Proposal Speaker SpeakerHistory ErrorUi LikeButton ShareButton TableOfContents BottomBar

    PullToRefreshBox ShareButton LikeButton NextProposalButton PrevProposalButton onClick onClick onClick onClick onClick onClick onPullToRefresh onRetryButtonClick onItemClick onUrlClick
  22. ProposalScreen AppBar Proposal Speaker SpeakerHistory ErrorUi LikeButton ShareButton TableOfContents BottomBar

    PullToRefreshBox ShareButton LikeButton NextProposalButton PrevProposalButton onClick onClick onClick onClick onClick onClick onPullToRefresh onRetryButtonClick onItemClick onUrlClick onHistoryItemClick onShareButtonClick onLikeButtonClick onShareButtonClick onPrevProposalButtonClick onNextProposalButtonClick onLikeButtonClick
  23. ProposalScreen AppBar Proposal Speaker SpeakerHistory ErrorUi LikeButton ShareButton TableOfContents BottomBar

    PullToRefreshBox ShareButton LikeButton NextProposalButton PrevProposalButton onClick onClick onClick onClick onClick onClick onPullToRefresh onRetryButtonClick onItemClick onUrlClick onHistoryItemClick onShareButtonClick onLikeButtonClick onShareButtonClick onPrevProposalButtonClick onNextProposalButtonClick onLikeButtonClick onAppBarShareButtonClick onBottomBarPrevProposalButtonClick onBottomBarNextProposalButtonClick onBottomBarLikeButtonClick onPullToRefresh onErrorRetryButtonClick onProposalLikeButtonClick onProposalShareButtonClick onSpeakerUrlClick onSpeakerHistoryItemClick
  24. @Composable fun LikeButton( onClick: () -> Unit, modifier: Modifier =

    Modifier, ) { IconButton( onClick = onClick, modifier = modifier ) { Icon( imageVector = Icons.Default.Favorite, contentDescription = "Like", ) } } Kotlin 汎用性が高い!
  25. @Composable internal fun Speaker( speaker: Proposal.Speaker, onUrlClick: (Uri) -> Unit,

    onHistoryItemClick: (Uri) -> Unit, modifier: Modifier = Modifier, ) { Column( modifier = modifier .padding(16.dp) ) { Text( text = "登壇者プロフィール", ) Column( modifier = Modifier Kotlin
  26. interface ProposalScreenEvents { fun onAppBarBackButtonClick() fun onAppBarShareButtonClick() fun onBottomBarPrevProposalButtonClick() fun

    onBottomBarNextProposalButtonClick() fun onBottomBarLikeButtonClick() fun onPullToRefresh() fun onErrorRetryButtonClick() fun onShareButtonClick() fun onLikeButtonClick() fun onSpeakerExpandButtonClick() fun onSpeakerUrlClick(url: Uri) fun onSpeakerHistoryItemClick(itemUrl: Uri) } Kotlin
  27. ProposalScreen( uiState = uiState, events = object : ProposalScreenEvents {

    override fun onAppBarBackButtonClick() { ... } override fun onAppBarShareButtonClick() { ... } override fun onBottomBarPrevProposalButtonClick() { ... } override fun onBottomBarNextProposalButtonClick() { ... } override fun onBottomBarLikeButtonClick() { ... } override fun onPullToRefresh() { ... } override fun onErrorRetryButtonClick() { ... } override fun onShareButtonClick() { ... } override fun onLikeButtonClick() { ... } override fun onSpeakerExpandButtonClick() { ... } override fun onSpeakerUrlClick(url: Uri) { ... } override fun onSpeakerHistoryItemClick(itemUrl: Uri) { ... } } ) Kotlin
  28. ProposalScreen( uiState = uiState, events = remember { object :

    ProposalScreenEvents { override fun onAppBarBackButtonClick() { ... } override fun onAppBarShareButtonClick() { ... } override fun onBottomBarPrevProposalButtonClick() { ... } override fun onBottomBarNextProposalButtonClick() { ... } override fun onBottomBarLikeButtonClick() { ... } override fun onPullToRefresh() { ... } override fun onErrorRetryButtonClick() { ... } override fun onShareButtonClick() { ... } override fun onLikeButtonClick() { ... } override fun onSpeakerExpandButtonClick() { ... } override fun onSpeakerUrlClick(url: Uri) { ... } override fun onSpeakerHistoryItemClick(itemUrl: Uri) { ... } } Kotlin
  29. ProposalScreen( uiState = uiState, events = remember(viewModel, coroutineScope, context) {

    object : ProposalScreenEvents { override fun onAppBarBackButtonClick() { ... } override fun onAppBarShareButtonClick() { ... } override fun onBottomBarPrevProposalButtonClick() { ... } override fun onBottomBarNextProposalButtonClick() { ... } override fun onBottomBarLikeButtonClick() { ... } override fun onPullToRefresh() { ... } override fun onErrorRetryButtonClick() { ... } override fun onShareButtonClick() { ... } override fun onLikeButtonClick() { ... } override fun onSpeakerExpandButtonClick() { ... } override fun onSpeakerUrlClick(url: Uri) { ... } override fun onSpeakerHistoryItemClick(itemUrl: Uri) { ... } } Kotlin
  30. @Composable internal fun ProposalScreen( uiState: ProposalUiState, events: ProposalScreenEvents, ) {

    Scaffold( topBar = { AppBar( events::onAppBarBackButtonClick, events::onAppBarShareButtonClick ) }, bottomBar = { BottomBar( events::onBottomBarPrevProposalButtonClick, events::onBottomBarNextProposalButtonClick, events::onBottomBarLikeButtonClick Kotlin
  31. @Composable internal fun ProposalScreen( uiState: ProposalUiState, events: ProposalScreenEvents, ) {

    Scaffold( topBar = { AppBar( events ) }, bottomBar = { BottomBar( events Kotlin
  32. -- MODEL type alias Model = { proposal: LoadingState Proposal

    } init : Model init = { proposal = Loading } -- MSG type Msg = ShareButtonClick | LikeButtonClick | SpeakerHistoryItemClick Uri -- UPDATE update : Msg -> Model -> Model Elm
  33. // MODEL data class Model( val proposal: LoadingState<Proposal> ) fun

    init(): Model = Model(Loading) // MSG sealed class Msg { data object ShareButtonClick : Msg() data object LikeButtonClick : Msg() data class SpeakerHistoryItemClick(val url: Uri) : Msg() } // UPDATE fun update(msg: Msg, model: Model): Model { return when (msg) { is Msg.ShareButtonClick -> model Kotlin
  34. fun view(model: Model): Html<Msg> { return div( attrs = listOf(),

    children = listOf( appBar(), when (model.proposal) { is Loading -> loadingView() is Error -> errorView() is Success -> div( attrs = listOf(), children = listOf( proposalView(model.proposal.value) ) ) }, bottomBar(), ) ) Kotlin
  35. fun likeButton(): Html<Msg> { return button( attrs = listOf( onClick(Msg::LikeButtonClick)

    ), children = listOf( img( attrs = listOf( src("like.png") ), children = listOf() ) ) ) } Kotlin
  36. fun likeButton(): Html<Msg> { return button( attrs = listOf( onClick(Msg::LikeButtonClick)

    ), children = listOf( img( attrs = listOf( src("like.png") ), children = listOf() ) ) ) } Kotlin イベント宣言が ない!
  37. sealed class Msg { data object AppBarBackButtonClick : Msg() data

    object AppBarShareButtonClick : Msg() data object BottomBarPrevProposalButtonClick : Msg() data object BottomBarNextProposalButtonClick : Msg() data object BottomBarLikeButtonClick : Msg() data object PullToRefresh : Msg() data object ErrorRetryButtonClick : Msg() data object ShareButtonClick : Msg() data object LikeButtonClick : Msg() data object SpeakerExpandButtonClick : Msg() data class SpeakerUrlClick(val uri: Uri) : Msg() data class SpeakerHistoryItemClick(val url: Uri) : Msg() } Kotlin
  38. fun likeButton(): Html<Msg> { return button( attrs = listOf( onClick(Msg::LikeButtonClick)

    ), children = listOf( img( attrs = listOf( src("like.png") ), children = listOf() ) ) ) } Kotlin
  39. fun update(msg: Msg, model: Model): Model { return when (msg)

    { is Msg.AppBarBackButtonClick -> ... is Msg.AppBarShareButtonClick -> ... is Msg.BottomBarPrevProposalButtonClick -> ... is Msg.BottomBarNextProposalButtonClick -> ... is Msg.BottomBarLikeButtonClick -> ... is Msg.PullToRefresh -> ... is Msg.ErrorRetryButtonClick -> ... is Msg.ShareButtonClick -> ... is Msg.LikeButtonClick -> ... is Msg.SpeakerExpandButtonClick -> ... is Msg.SpeakerUrlClick -> ... is Msg.SpeakerHistoryItemClick -> ... } } Kotlin
  40. fun update(msg: Msg, model: Model): Model { return when (msg)

    { is Msg.AppBarBackButtonClick -> ... is Msg.AppBarShareButtonClick -> ... is Msg.BottomBarPrevProposalButtonClick -> ... is Msg.BottomBarNextProposalButtonClick -> ... is Msg.BottomBarLikeButtonClick -> ... is Msg.PullToRefresh -> ... is Msg.ErrorRetryButtonClick -> ... is Msg.ShareButtonClick -> ... is Msg.LikeButtonClick -> ... is Msg.SpeakerExpandButtonClick -> ... is Msg.SpeakerUrlClick -> ... is Msg.SpeakerHistoryItemClick -> ... } } Kotlin 発生したMsg 新しいModel 現在のModel
  41. fun update(msg: Msg, model: Model): Model { return when (msg)

    { is Msg.AppBarBackButtonClick -> ... is Msg.AppBarShareButtonClick -> ... is Msg.BottomBarPrevProposalButtonClick -> ... is Msg.BottomBarNextProposalButtonClick -> ... is Msg.BottomBarLikeButtonClick -> ... is Msg.PullToRefresh -> ... is Msg.ErrorRetryButtonClick -> ... is Msg.ShareButtonClick -> ... is Msg.LikeButtonClick -> { model.copy(proposal = いいね完了後の状態) } is Msg.SpeakerExpandButtonClick -> ... is Msg.SpeakerUrlClick -> ... is Msg.SpeakerHistoryItemClick -> ... } Kotlin
  42. sealed class Msg { data object AppBarBackButtonClick : Msg() data

    object BottomBarPrevProposalButtonClick : Msg() data object BottomBarNextProposalButtonClick : Msg() data object PullToRefresh : Msg() data object ErrorRetryButtonClick : Msg() data object ShareButtonClick : Msg() data object LikeButtonClick : Msg() data class SpeakerHistoryItemClick(val url: Uri) : Msg() } fun view(model: Model): Html<Msg> fun proposalView(proposal: Proposal): Html<Msg> fun speakerView(speaker: Speaker): Html<Msg> Kotlin イベントはここにま とめられる イベント宣言なし
  43. fun view(model: Model): Html<Msg> { return div( attrs = listOf(),

    children = listOf( appBar(), when (model.proposal) { is Loading -> loadingView() is Error -> errorView() is Success -> div( attrs = listOf(), children = listOf( proposalView(model.proposal.value) ) ) }, bottomBar(), ) ) Kotlin @Composable fun View(model: Model) { Column { AppBar() when (model.proposal) { is Loading -> { LoadingUi() } is Error -> { ErrorUi() } is Success -> { Proposal( proposal = model. ) } } BottomBar()
  44. sealed class Msg { data object AppBarBackButtonClick : Msg() data

    object AppBarShareButtonClick : Msg() data object BottomBarPrevProposalButtonClick : Msg() data object BottomBarNextProposalButtonClick : Msg() data object BottomBarLikeButtonClick : Msg() data object PullToRefresh : Msg() data object ErrorRetryButtonClick : Msg() data object ShareButtonClick : Msg() data object LikeButtonClick : Msg() data object SpeakerExpandButtonClick : Msg() data class SpeakerUrlClick(val uri: Uri) : Msg() data class SpeakerHistoryItemClick(val url: Uri) : Msg() } Kotlin
  45. fun update(msg: Msg, model: Model): Model { return when (msg)

    { is Msg.AppBarBackButtonClick -> ... is Msg.AppBarShareButtonClick -> ... is Msg.BottomBarPrevProposalButtonClick -> ... is Msg.BottomBarNextProposalButtonClick -> ... is Msg.BottomBarLikeButtonClick -> ... is Msg.PullToRefresh -> ... is Msg.ErrorRetryButtonClick -> ... is Msg.ShareButtonClick -> ... is Msg.LikeButtonClick -> ... is Msg.SpeakerExpandButtonClick -> ... is Msg.SpeakerUrlClick -> ... is Msg.SpeakerHistoryItemClick -> ... } } Kotlin
  46. @Composable private fun LikeButton( modifier: Modifier = Modifier, ) {

    IconButton( onClick = { }, modifier = modifier ) { Icon( imageVector = Icons.Default.FavoriteBorder, contentDescription = "Like", ) } } Kotlin
  47. @Composable private fun LikeButton( modifier: Modifier = Modifier, ) {

    IconButton( onClick = { model = update(Msg.LikeButtonClick, model) }, modifier = modifier ) { Icon( imageVector = Icons.Default.FavoriteBorder, contentDescription = "Like", ) } } Kotlin
  48. @Composable private fun LikeButton( onMsg: (Msg) -> Unit, modifier: Modifier

    = Modifier, ) { IconButton( onClick = { model = update(Msg.LikeButtonClick, model) }, modifier = modifier ) { Icon( imageVector = Icons.Default.FavoriteBorder, contentDescription = "Like", ) } } Kotlin 引数を追加
  49. var model by remember { mutableStateOf(Model(Loading)) } View( model =

    model, onMsg = { msg -> model = update(msg, model) } ) @Composable private fun LikeButton( onMsg: (Msg) -> Unit, modifier: Modifier = Modifier, ) { IconButton( onClick = { model = update(Msg.LikeButtonClick, model) Kotlin 引数を追加 updateを 呼べる!
  50. var model by remember { mutableStateOf(Model(Loading)) } View( model =

    model, onMsg = { msg -> model = update(msg, model) } ) @Composable private fun LikeButton( onMsg: (Msg) -> Unit, modifier: Modifier = Modifier, ) { IconButton( onClick = { onMsg(Msg.LikeButtonClick) } Kotlin
  51. var model by remember { mutableStateOf(Model(Loading)) } View( model =

    model, onMsg = { msg -> model = update(msg, model) } ) @Composable private fun LikeButton( onMsg: (Msg) -> Unit, modifier: Modifier = Modifier, ) { IconButton( onClick = { onMsg(Msg.LikeButtonClick) } Kotlin LikeButtonClick
  52. @Composable fun View( model: Model, onMsg: (Msg) -> Unit )

    { 途中のComposable( onMsg = onMsg ) } @Composable fun 途中のComposable( onMsg: (Msg) -> Unit ) { LikeButton( onMsg = onMsg ) } Kotlin
  53. @Composable private fun LikeButton(onMsg: (Msg) -> Unit) { Icon( imageVector

    = Icons.Default.FavoriteBorder, modifier = modifier .pointerInput(Unit) { awaitEachGesture { detectGestures( onClick = {}, onLongClick = {}, onDoubleClick = {}, onSwipeLeft = {}, onSwipeUp = {}, onSwipeRight = {}, onSwipeBottom = {} ) } } Kotlin
  54. @Composable private fun LikeButton(onMsg: (Msg) -> Unit) { Icon( imageVector

    = Icons.Default.FavoriteBorder, modifier = modifier .pointerInput(Unit) { awaitEachGesture { detectGestures( onClick = { onMsg(Msg.LikeButtonClick) }, onLongClick = { onMsg(Msg.LikeButtonLongClick) }, onDoubleClick = { onMsg(Msg.LikeButtonDoubleClick) }, onSwipeLeft = { onMsg(Msg.LikeButtonSwipeLeft) }, onSwipeUp = { onMsg(Msg.LikeButtonSwipeUp) }, onSwipeRight = { onMsg(Msg.LikeButtonSwipeRight) }, onSwipeBottom = { onMsg(Msg.LikeButtonSwipeBottom) } ) } } Kotlin
  55. @Composable private fun LikeButton(onMsg: (Msg) -> Unit) { Icon( imageVector

    = Icons.Default.FavoriteBorder, modifier = modifier .pointerInput(Unit) { awaitEachGesture { detectGestures( onClick = { onMsg(Msg.LikeButtonClick) }, onLongClick = { onMsg(Msg.LikeButtonLongClick) }, onDoubleClick = { onMsg(Msg.LikeButtonDoubleClick) }, onSwipeLeft = { onMsg(Msg.LikeButtonSwipeLeft) }, onSwipeUp = { onMsg(Msg.LikeButtonSwipeUp) }, onSwipeRight = { onMsg(Msg.LikeButtonSwipeRight) }, onSwipeBottom = { onMsg(Msg.LikeButtonSwipeBottom) } ) } } Kotlin 7種類のイベントすべてを onMsgひとつで捌く!
  56. ProposalScreen AppBar Proposal Speaker SpeakerHistory ErrorUi LikeButton ShareButton TableOfContents BottomBar

    PullToRefreshBox ShareButton LikeButton NextProposalButton PrevProposalButton onClick onClick onClick onClick onClick onClick onPullToRefresh onRetryButtonClick onItemClick onUrlClick onHistoryItemClick onShareButtonClick onLikeButtonClick onShareButtonClick onPrevProposalButtonClick onNextProposalButtonClick onLikeButtonClick onAppBarShareButtonClick onBottomBarPrevProposalButtonClick onBottomBarNextProposalButtonClick onBottomBarLikeButtonClick onPullToRefresh onErrorRetryButtonClick onProposalLikeButtonClick onProposalShareButtonClick onSpeakerUrlClick onSpeakerHistoryItemClick
  57. ProposalScreen AppBar Proposal Speaker SpeakerHistory ErrorUi LikeButton ShareButton TableOfContents BottomBar

    PullToRefreshBox ShareButton LikeButton NextProposalButton PrevProposalButton onMsg onMsg onMsg onMsg onMsg onMsg onMsg onMsg onMsg onMsg onMsg onMsg onMsg onMsg
  58. @Composable fun LikeButton( onMsg: (Msg) -> Unit, modifier: Modifier =

    Modifier, ) { IconButton( onClick = { onMsg(Msg.LikeButtonClick) }, modifier = modifier ) { Icon( imageVector = Icons.Default.FavoriteBorder, contentDescription = "Like", ) } } Kotlin
  59. @Composable fun LikeButton( onMsg: (Msg) -> Unit, modifier: Modifier =

    Modifier, ) { IconButton( onClick = { onMsg(Msg.LikeButtonClick) }, modifier = modifier ) { Icon( imageVector = Icons.Default.FavoriteBorder, contentDescription = "Like", ) } } Kotlin
  60. @Composable fun LikeButton( onMsg: (Msg) -> Unit, modifier: Modifier =

    Modifier, ) { IconButton( onClick = { onMsg(Msg.LikeButtonClick) }, modifier = modifier ) { Icon( imageVector = Icons.Default.FavoriteBorder, contentDescription = "Like", ) } } Kotlin 実際にはこれしか 発生しない
  61. sealed class Msg { data object AppBarBackButtonClick : Msg() data

    object AppBarShareButtonClick : Msg() data object BottomBarPrevProposalButtonClick : Msg() data object BottomBarNextProposalButtonClick : Msg() data object BottomBarLikeButtonClick : Msg() data object PullToRefresh : Msg() data object ErrorRetryButtonClick : Msg() data object ShareButtonClick : Msg() data object LikeButtonClick : Msg() data object SpeakerExpandButtonClick : Msg() data class SpeakerUrlClick(val uri: Uri) : Msg() data class SpeakerHistoryItemClick(val url: Uri) : Msg() } Kotlin
  62. sealed class Msg { sealed class AppBarMsg : Msg() {

    data object BackButtonClick : AppBarMsg() data object ShareButtonClick : AppBarMsg() } sealed class BottomBarMsg : Msg() { data object PrevProposalButtonClick : BottomBarMsg() data object NextProposalButtonClick : BottomBarMsg() data object LikeButtonClick : BottomBarMsg() } data object PullToRefresh : Msg() data object ShareButtonClick : Msg() data object LikeButtonClick : Msg() sealed class SpeakerMsg : Msg() { data class UrlClick(val uri: Uri) : SpeakerMsg() sealed class HistoryMsg : SpeakerMsg() { data class ItemClick(val url: Uri) : HistoryMsg() Kotlin
  63. @Composable internal fun AppBar( onMsg: (AppBarMsg) -> Unit ) @Composable

    internal fun BottomBar( onMsg: (BottomBarMsg) -> Unit ) Kotlin
  64. sealed class AppBarMsg : Msg() { data object BackButtonClick :

    AppBarMsg() data object ShareButtonClick : AppBarMsg() } @Composable internal fun AppBar( onMsg: (AppBarMsg) -> Unit ) sealed class BottomBarMsg : Msg() { data object PrevProposalButtonClick : BottomBarMsg() data object NextProposalButtonClick : BottomBarMsg() data object LikeButtonClick : BottomBarMsg() } @Composable internal fun BottomBar( onMsg: (BottomBarMsg) -> Unit ) Kotlin
  65. @Composable internal fun AppBar( onMsg: (AppBarMsg) -> Unit ) @Composable

    internal fun ProposalScreen( uiState: ProposalUiState, onMsg: (Msg) -> Unit ) { AppBar( onMsg = onMsg ) } Kotlin 型が一致しない
  66. (A, B, C) -> R 共変 反変 Function<in A, in

    B, in C, out R> Function<? super A, ? super B, ? super C, ? extends R> と同じ!
  67. @Composable internal fun AppBar( onMsg: (AppBarMsg) -> Unit ) @Composable

    internal fun ProposalScreen( uiState: ProposalUiState, onMsg: (Msg) -> Unit ) { AppBar( onMsg = onMsg ) } Kotlin そのまま渡せる!
  68. @Composable internal fun AppBar( onMsg: (AppBarMsg) -> Unit ) @Composable

    internal fun ProposalScreen( uiState: ProposalUiState, onMsg: (Msg) -> Unit ) { AppBar( onMsg = onMsg ) } Kotlin 型が一致しない そのまま渡せる!
  69. View( model = model, onMsg = { msg -> model

    = update(msg, model) } ) fun update(msg: Msg, model: Model): Model { return when (msg) { is AppBarMsg.BackButtonClick -> ... is AppBarMsg.ShareButtonClick -> ... is BottomBarMsg.PrevProposalButtonClick -> ... is BottomBarMsg.NextProposalButtonClick -> ... is Msg.PullToRefresh -> ... is Msg.ErrorRetryButtonClick -> ... is Msg.LikeButtonClick -> ... is Msg.SpeakerMsg.UrlClick -> ... is Msg.SpeakerMsg.HistoryMsg.ItemClick -> ... Kotlin
  70. View( model = model, onMsg = { msg -> model

    = when (msg) { is AppBarMsg.BackButtonClick -> ... is AppBarMsg.ShareButtonClick -> ... is BottomBarMsg.PrevProposalButtonClick -> ... is BottomBarMsg.NextProposalButtonClick -> ... is Msg.PullToRefresh -> ... is Msg.ErrorRetryButtonClick -> ... is Msg.LikeButtonClick -> ... is Msg.SpeakerMsg.UrlClick -> ... is Msg.SpeakerMsg.HistoryMsg.ItemClick -> ... } } ) Kotlin
  71. View( model = model, onMsg = { msg -> when

    (msg) { is AppBarMsg.BackButtonClick -> ... is AppBarMsg.ShareButtonClick -> ... is BottomBarMsg.PrevProposalButtonClick -> ... is BottomBarMsg.NextProposalButtonClick -> ... is Msg.PullToRefresh -> ... is Msg.ErrorRetryButtonClick -> ... is Msg.LikeButtonClick -> { viewModel.likeProposal() } is Msg.SpeakerMsg.UrlClick -> ... is Msg.SpeakerMsg.HistoryMsg.ItemClick -> ... } } ) Kotlin
  72. val viewModel = TODO() val model = viewModel.model ProposalScreen( model

    = model, onMsg = { msg -> when (msg) { is AppBarMsg.BackButtonClick -> { navController.popBackStack() } is BottomBarMsg.LikeButtonClick -> { viewModel.likeProposal() } is Msg.PullToRefresh -> { viewModel.reloadProposal() } is Msg.ErrorRetryButtonClick -> { Kotlin
  73. val viewModel = TODO() val model = viewModel.model ProposalScreen( model

    = model, onMsg = { msg -> when (msg) { is AppBarMsg.BackButtonClick -> { navController.popBackStack() } is BottomBarMsg.LikeButtonClick -> { viewModel.likeProposal() } is Msg.PullToRefresh -> { viewModel.reloadProposal() } is Msg.ErrorRetryButtonClick -> { Kotlin
  74. val viewModel: ProposalViewModel = koinViewModel() val model = viewModel.model ProposalScreen(

    model = model, onMsg = { msg -> when (msg) { is AppBarMsg.BackButtonClick -> { navController.popBackStack() } is BottomBarMsg.LikeButtonClick -> { viewModel.likeProposal() } is Msg.PullToRefresh -> { viewModel.reloadProposal() } is Msg.ErrorRetryButtonClick -> { Kotlin
  75. val viewModel: ProposalViewModel = koinViewModel() val model = viewModel.model ProposalScreen(

    model = model, onMsg = { msg -> when (msg) { is AppBarMsg.BackButtonClick -> { navController.popBackStack() } is BottomBarMsg.LikeButtonClick -> { viewModel.likeProposal() } is Msg.PullToRefresh -> { viewModel.reloadProposal() } is Msg.ErrorRetryButtonClick -> { Kotlin
  76. val viewModel: ProposalViewModel = koinViewModel() val model = viewModel.model ProposalScreen(

    model = model, onMsg = { msg -> when (msg) { is AppBarMsg.BackButtonClick -> { navController.popBackStack() } is BottomBarMsg.LikeButtonClick -> { viewModel.likeProposal() } is Msg.PullToRefresh -> { viewModel.reloadProposal() } is Msg.ErrorRetryButtonClick -> { Kotlin
  77. val viewModel: ProposalViewModel = koinViewModel() val model = viewModel.model ProposalScreen(

    model = model, onMsg = { msg -> when (msg) { is AppBarMsg.BackButtonClick -> { navController.popBackStack() } is BottomBarMsg.LikeButtonClick -> { viewModel.likeProposal() } is Msg.PullToRefresh -> { viewModel.reloadProposal() } is Msg.ErrorRetryButtonClick -> { Kotlin
  78. val viewModel: ProposalViewModel = koinViewModel() val model = viewModel.model ProposalScreen(

    model = model, onMsg = { msg -> when (msg) { is AppBarMsg.BackButtonClick -> { navController.popBackStack() } is BottomBarMsg.LikeButtonClick -> { viewModel.likeProposal() } is Msg.PullToRefresh -> { viewModel.reloadProposal() } is Msg.ErrorRetryButtonClick -> { Kotlin
  79. val viewModel: ProposalViewModel = koinViewModel() val model = viewModel.model ProposalScreen(

    model = model, onMsg = { msg -> when (msg) { is AppBarMsg.BackButtonClick -> { navController.popBackStack() } is BottomBarMsg.LikeButtonClick -> { viewModel.likeProposal() } is Msg.PullToRefresh -> { viewModel.reloadProposal() } is Msg.ErrorRetryButtonClick -> { Kotlin Recomposed!
  80. @Immutable sealed class ProposalMsg { data object PullToRefresh : ProposalMsg()

    data object ErrorRetryButtonClick : ProposalMsg() data object ShareButtonClick : ProposalMsg() data object LikeButtonClick : ProposalMsg() } @Composable internal fun ProposalScreen( model: ProposalModel, onMsg: (ProposalMsg) -> Unit ) Kotlin
  81. @Immutable sealed class ProposalMsg { data object PullToRefresh : ProposalMsg()

    data object ErrorRetryButtonClick : ProposalMsg() data object ShareButtonClick : ProposalMsg() data object LikeButtonClick : ProposalMsg() } @Composable internal fun ProposalScreen( model: ProposalModel, onMsg: (ProposalMsg) -> Unit ) Kotlin
  82. @Immutable sealed class SpeakerMsg : ProposalMsg() { data object ExpandButtonClick

    : SpeakerMsg() data class UrlClick(val uri: Uri) : SpeakerMsg() } @Composable internal fun Speaker( speaker: Proposal.Speaker, onMsg: (SpeakerMsg) -> Unit ) Kotlin
  83. @Immutable sealed class HistoryMsg : SpeakerMsg() { data class ItemClick(val

    item: Speaker.History.Item) : HistoryMsg() } @Composable private fun SpeakerHistory( history: Proposal.Speaker.History, onMsg: (HistoryMsg) -> Unit ) Kotlin
  84. val viewModel: ProposalViewModel = koinViewModel() val model = viewModel.model ProposalScreen(

    model = model, onMsg = { msg -> when (msg) { is AppBarMsg.BackButtonClick -> { navController.popBackStack() } is BottomBarMsg.LikeButtonClick -> { viewModel.likeProposal() } is Msg.PullToRefresh -> { viewModel.reloadProposal() } is Msg.ErrorRetryButtonClick -> { Kotlin
  85. 3. こんな画面は存在しない ProposalScreen AppBar Proposal Speaker SpeakerHistory ErrorUi LikeButton ShareButton

    TableOfContents BottomBar PullToRefreshBox ShareButton LikeButton NextProposalButton PrevProposalButton onClick onClick onClick onClick onClick onClick onPullToRefresh onRetryButtonClick onItemClick onUrlClick onHistoryItemClick onShareButtonClick onLikeButtonClick onShareButtonClick onPrevProposalButtonClick onNextProposalButtonClick onLikeButtonClick onAppBarShareButtonClick onBottomBarPrevProposalButtonClick onBottomBarNextProposalButtonClick onBottomBarLikeButtonClick onPullToRefresh onErrorRetryButtonClick onProposalLikeButtonClick onProposalShareButtonClick onSpeakerUrlClick onSpeakerHistoryItemClick
  86. 部分的に導入することも可能 こんな画面に遭遇したときだけ使う ProposalScreen AppBar Proposal Speaker SpeakerHistory ErrorUi LikeButton ShareButton

    TableOfContents BottomBar PullToRefreshBox ShareButton LikeButton NextProposalButton PrevProposalButton onClick onClick onClick onClick onClick onClick onPullToRefresh onRetryButtonClick onItemClick onUrlClick onHistoryItemClick onShareButtonClick onLikeButtonClick onShareButtonClick onPrevProposalButtonClick onNextProposalButtonClick onLikeButtonClick onAppBarShareButtonClick onBottomBarPrevProposalButtonClick onBottomBarNextProposalButtonClick onBottomBarLikeButtonClick onPullToRefresh onErrorRetryButtonClick onProposalLikeButtonClick onProposalShareButtonClick onSpeakerUrlClick onSpeakerHistoryItemClick
  87. onAppBarNavigationButtonClick: () -> Unit, onAppBarShareButtonClick: () -> Unit, onBottomControllerLikeButtonClick: ()

    -> Unit, onBottomControllerNextEpisodeButtonClick: () -> Unit, onBottomControllerPreviousEpisodeButtonClick: () -> Unit, onBottomControllerOrientationSwitchButtonClick: () -> Unit, onReloadButtonClick: () -> Unit, onPageSelected: (index: Int) -> Unit, onPagerContentClick: (index: Int) -> Unit, onPagerPreviousClick: (isMultipleTapping: Boolean) -> Unit, onPagerNextClick: (isMultipleTapping: Boolean) -> Unit, onPagerOverScrolled: () -> Unit, onLastPageLikeButtonClick: () -> Unit, onLastPageFavoriteButtonClick: () -> Unit, onLastPageShareButtonClick: () -> Unit, onLastPageFanletterWriteButtonClick: () -> Unit, onLastPageFanletterViewButtonClick: () -> Unit, onLastPageRecommendComicClick: (recommendComic: Comic) -> Unit, onLastPageNextEpisodeButtonClick: () -> Unit, Kotlin onMsg: (ViewerMsg) -> Unit,