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

Modeling UiEvent

mikan
July 24, 2024

Modeling UiEvent

モバチキ 〜Mobile Tips 共有会〜 #5
https://karabiner.connpass.com/event/323020/

mikan

July 24, 2024
Tweet

More Decks by mikan

Other Decks in Technology

Transcript

  1. 自己紹介 object Mikan { val name = " 一瀬喜弘" val

    company = "karabiner.tech" val work = Engineer.Android val hobby = listOf( " 漫画", " アニメ", " ゲーム", " 折り紙", "OSS 開発・コントリビュート", ) }
  2. 実務のコードだと最大で 72 個 のパラメータを持っており、今後も増えていく予定 @Composable fun カート画面( slot: @Composable ()

    -> Unit, aUiModel: AUiModel, contents: List<Content>, onClickA: (id: ContentId) -> Unit, onClickA2: (id: ContentId, index: Int) -> Unit, bannerUiModel: BannerUiModel, onClickBanner: () -> Unit, notifications: List<Notification>, onClickNotification: (messageId: String, url: String) -> Unit, // ... )
  3. 問題: 既存のユーザーインタラクションを消すとき 1. ユーザーインタラクションを発火している箇所を消す 2. それを呼び出しているコードを修正 3. それを呼び出しているコードを修正 4. それを(ry

    5. ViewModel 側の処理が不要になったのならそれも削除する ユーザーインタラクションを消すという単純なことなのに バケツリレーのすべてが影響を受け、変更容易性がだんだん下がる
  4. ユーザーインタラクションをモデリング ユーザーインタラクションを HogeUiEvent という sealed interface として直積的に実装する UiEvent の種類 クリック

    おそらくこれがほとんど キーボード入力 sealed interface CartUiEvent { data class ClickA(id: ContentId) : CartUiEvent data object ClickBanner : CartUiEvent // ... }
  5. 1. ViewModel にイベントハンドラーを定義する class CartViewModel() : ViewModel() { // エントリーポイントだけ外部に公開する

    fun onUiEvent(event: CartUiEvent) { // UiEvent と具体的な処理をマッピングする when (event) { is CartUiEvent.ClickA -> onClickA(event.id) CartUiEvent.ClickBanner -> onClickBanner() // ... } } // 具体的な処理はすべてprivate で隠蔽する private fun onClickA(id: ContentId) { // ... } private fun onClickBanner() { // } // ... }
  6. UiEvent sealed interface CartUiEvent { data class ChangeQuantity(itemId: String, quantity:

    Int): CartUiEven data class DeleteItem(itemId: String) : CartUiEvent data class ClickAd(url: String) : CartUiEvent }
  7. ViewModel class CartViewModel() : ViewModel() { fun onUiEvent(event: CartUiEvent) {

    when (event) { is ChangeQuantity -> changeQuantity(event.itemId, event.quantity) is DeleteItem -> deleteItem(event.itemId) is ClickAd -> transitionToWebView(event.url) } } private fun changeQuantity(itemId: String, quantity: Int) { // ... } private fun deleteItem(itemId: String) { // ... } private fun transitionToWebView(url: String) { // ... } }
  8. Activity class CartActivity() : ComponentActivity() { pruivate val viewModel: CartViewModel

    by viewModels() override fun onCreate(savedInstanceState: Bundle?) { setContent { MyAppTheme { Surface { CartScreen( // ... onUiEvent = viewModel::onUiEvent ) } } } } }
  9. Parent Component @Composable fun CartScreen( // ... onUiEvent: (CartUiEvent) ->

    Unit, ) { LazyColumn { items(uiModel.items, { it.id }) { item -> CartItem( // ... onUiEvent = onUiEvent ) } item { AdSection( // ... onUiEvent = onUiEvent ) } } }
  10. Child Component @Composable fun CartItem( // ... onUiEvent: (CartUiEvent) ->

    Unit ) { // ... Spinner( onQuantityChange = { quantity -> onUiEvent(ChangeQuantity(uiModel.itemId, quantity)) } ) DeleteButton( onClick = { onUiEvent(DeleteItem(uiModel.id)) } ) }
  11. UiEvent イベント追加 sealed interface CartUiEvent { data class ChangeQuantity(itemId: String,

    quantity: Int): CartUiEven data class DeleteItem(itemId: String) : CartUiEvent data class ClickAd(url: String) : CartUiEvent data class AddToFavorite(itemId: String) : CartUiEvent }
  12. ViewModel イベントハンドラー追加 UiEvent とイベントハンドラーのマッピング追加 class CartViewModel() : ViewModel() { fun

    onUiEvent(event: CartUiEvent) { when (event) { is ChangeQuantity -> changeQuantity(event.itemId, event.quantity) is DeleteItem -> deleteItem(event.itemId) is ClickAd -> transitionToWebView(event.url) is AddToFavorite -> addToFavorite(event.itemId) } } // ... private fun addToFavorite(itemId: String) { // ... } }
  13. Activity class CartActivity() : ComponentActivity() { pruivate val viewModel: CartViewModel

    by viewModels() override fun onCreate(savedInstanceState: Bundle?) { setContent { MyAppTheme { Surface { CartScreen( // ... onUiEvent = viewModel::onUiEvent ) } } } } }
  14. Parent Component @Composable fun CartScreen( // ... onUiEvent: (CartUiEvent) ->

    Unit, ) { LazyColumn { items(uiModel.items, { it.id }) { item -> CartItem( // ... onUiEvent = onUiEvent ) } item { AdSection( // ... onUiEvent = onUiEvent ) } } }
  15. Child Component コンポーネント追加 イベント発火 @Composable fun CartItem( // ... onUiEvent:

    (CartUiEvent) -> Unit ) { // ... FavoriteButton( onClick = { onUiEvent(AddToFavorite(uiModel.id)) } ) }
  16. UiEvent を構造化する: 複雑化するUI への対処 UI 要素が増えてUiEvent がどんどん複雑化していったとき ClickMoreA とClickMoreB は名前が非常に似ているので取り違える可能性が高い

    → 実行できるUiEvent に制限を設けて間違いを減らす sealed interface CartUiEvent { data class ChangeQuantity(val itemId: String, val quantity: Int) : C data class DeleteItem(val itemId: String) : CartUiEvent data class ClickAd(val url: String) : CartUiEvent data class AddToFavorite(itemId: String) : CartUiEvent data object ClickMoreA : CartUiEvent data object ClickMoreB : CartUiEvent }
  17. UiEvent を構造化する: 複雑化するUI への対処 UiEvent を継承する sealed interface CartUiEvent sealed

    interface ASectionUiEvent : CartUiEvent { data object ClickMore : ASectionUiEvent } sealed interface BSectionUiEvent : CartUiEvent { data object ClickMore : BSectionUiEvent }
  18. UiEvent を構造化する: 複雑化するUI への対処 Section レベルのComposable 関数に渡すUiEvent を制限する @Composable fun

    ASection( onUiEvent: (ASectionUiEvent) -> Unit, // ... ) { // ASectionUiEvent を実装したイベントしか発行できない } @Composable fun BSection( onUiEvent: (BSectionUiEvent) -> Unit, // ... ) { // BSectionUiEvent を実装したイベントしか発行できない }
  19. UiEvent を構造化する: 複雑化するUI への対処 CartUiEvent を継承しているのでonUiEvent をそのまま渡せる 🎉🎉 @Composable fun

    CartScreen( onUiEvent: (CartUiEvent) -> Unit, // ... ) { // CartUiEvent は継承元なのでそのまま渡すことができる ASection( // ... onUiEvent = onUiEvent ) BSection( // ... onUiEvent = onUiEvent ) }
  20. 共通パーツのUiEvent 複数の​ 画面で​ 使い回す共通の​ UI は​ どうしよう? このような​ UI には​

    共通の​ UiEvent は​ 定義せずに​ 利用する​ 側で​ それぞれUiEvent を​ 定義していく​ ほうが​ いい​ 気が​ している​ → 機能要件は​ 同じでも、​ 非機能要件​ (ログとか)は​ 画面ごとに​ やりたいことが​ 若干違ったりする​
  21. UiEvent がオーバーエンジニアリングしないように注意する このようなMVI フレームワークでは 状態、イベント、副作用を型 引数としたインターフェースなり抽象クラス(Screen<State, Event, Effect> )を作っていたり、 Redux

    アーキテクチャよろ しくReducer という概念を導入してくるが、 これらはいまのとこ ろ個人的にはやりすぎだと思っている 一貫性をもたせるために導入するには複雑すぎる Google が方針転換したり新しい実装パターンを提示した瞬間に 技術負債と化す
  22. 参考 https://medium.com/@hunterfreas/handling-ui-events-in-jetpack-compose-a-clean-approach-c8fd1bfc6231 iOSDC Japan 2020: SwiftUI 時代の Functional iOS Architecture

    / 稲見 泰宏 https://www.youtube.com/watch?v=g_hq3qfn-O8 圏論( モナド) にまで手をだしているので上級者向け → 関数型の考えは参考になる