$30 off During Our Annual Pro Sale. View Details »

_アニメーション抜き__MVIに基づくStateMachineアーキテクチャ_KMPとJetpack_ComposeとSwiftUIを組み合わせる.pdf

Marco Valentino
September 15, 2023

 _アニメーション抜き__MVIに基づくStateMachineアーキテクチャ_KMPとJetpack_ComposeとSwiftUIを組み合わせる.pdf

目次:
- KMMとJetpack ComposeとSwiftUIの紹介
- MVIアーキテクチャ
- なぜMVIを使用するのか
- MVIの実装方法
- MVIを使って画面を実装する
- MVIに基くStateMachine
- Jetpack Compose & SwiftUI の導入
- Jetpack Compose上の使い方
- SwiftUI上の使い方
- StateMachine DSL
- StateMachine DSL を使ってリファクタリングする
- 開発上のメリット・デメリット

Marco Valentino

September 15, 2023
Tweet

More Decks by Marco Valentino

Other Decks in Programming

Transcript

  1. MVIʹجͮ͘StateMachineΞʔΩςΫνϟ
    Kotlin Multiplatform &


    Jetpack Compose & SwiftUI


    Λ૊Έ߹ΘͤΔ
    DroidKaigi 2023


    マルコ&そば屋
    1

    View Slide

  2. 2
    Marco


    Valentino
    ߳ߓੜ·ΕɺதࠃҭͪɺΞϝϦΧͷେֶʹߦ͖ɺ೔ຊͰब৬͠·ͨ͠ɻ
    ڈ೥͔ΒגࣜձࣾΏΊΈͷAndroidςοΫϦʔυͱͯ͠ಇ͍͍ͯ·͢ɻ
    2020೥ͷ಄͔ΒKotlin MultiplatformΛϓϩδΣΫτʹಋೖ͠ɺͦΕҎདྷ
    ͜ͷςΫϊϩδʔʹऔΓ૊ΜͰ͍·͢ɻ
    wasabi-muffin wasabimuffin8

    View Slide

  3. 3
    sobaya-0141 sobaya15
    ͦ͹԰


    גࣜձࣾΏΊΈͰAndroidςοΫϦʔυʢ͓স͍୲౰ʣΛ͠ͳ͕Βٕज़
    ޿ใΛ͍ͯ͠·͢ɻ
    ຖ೔ೋ೔ਲ͍ͳͷͰɺۤͦ͠͏ʹ͍ͯͨ͠Β༏͍ͯͩ͘͘͠͠͞
    ※ SNSͰ΋ͳΜͰ΋͓ؾܰʹབྷΜͰ͍ͩ͘͞ʂʂ

    View Slide

  4. ςΫϊϩδʔͷ঺հ
    1
    ▶︎
    K o t l i n M u l t i p
    l a
    t f
    o r
    m
    の紹介


    ▶︎
    J e t p
    a
    c
    k C o m
    p
    o s
    e
    の紹介


    ▶︎
    S w i f
    t U
    I
    の紹介
    MVI ΞʔΩςΫνϟ
    2
    ▶︎
    M V
    I
    の紹介


    ▶︎なぜ�
    M V
    I
    �を使用するのか


    ▶︎
    M V
    I
    の実装方法
    ( K M P )

    ▶︎
    M V
    I
    を使って画面を実装する


    ▶︎
    M V
    I
    に基づく
    S t a
    t e M a
    c
    h i n e
    State Machine DSL
    4
    ▶︎
    S t a
    t e M a
    c
    h i n e D S L
    �を使って


    リファクタリングする


    ▶︎開発上のメリット・デメリット
    Jetpack Compose &
    SwiftUIͷಋೖ
    3
    ▶︎
    J e t p
    a
    c
    k C o m
    p
    o s
    e
    上の使い方


    ▶︎
    S w i f
    t U
    I
    上の使い方
    4


    View Slide

  5. ςΫϊϩδʔͷ঺հ

    View Slide

  6. 5
    ςΫϊϩδʔͷ঺հ

    View Slide

  7. 6
    ςΫϊϩδʔͷ঺հ
    ▶︎ コードを削減


    ▶︎ 直感的


    ▶︎ 開発を加速させる


    ▶︎ パワフル
    ▶︎ 高度なアニメーション制御


    ▶︎ シンプルになったデータフロー


    ▶︎
    A
    P I
    の拡


    ▶︎ 新タイプのグラフとインタラクティブな機能
    Jetpack Compose SwiftUI

    View Slide

  8. MVIΞʔΩςΫνϟͷ঺հ

    View Slide

  9. # MVIͷ঺հ
    Model
    View Intent
    dispatch
    update
    notify
    7

    View Slide

  10. # MVIͷ঺հ
    dispatch
    notify
    intent
    state
    Processor
    Reducer
    emit
    action
    Android / iOS
    send
    event
    8

    View Slide

  11. 9 MVIͷ঺հ
    intent
    state
    Reducer
    action
    Processor
    Component
    Contract

    View Slide

  12. 10 MVIͷ঺հ
    Immutability
    1
    Unidirectional Data Flow
    2

    View Slide

  13. MVIΞʔΩςΫνϟͷ࣮૷
    https://github.com/fika-tech/Macaron
    スライドに収めるために正しいコード


    スタイル使っていません
    !

    View Slide

  14. sealed interface Contract


    interface Intent : Contract


    interface Action : Contract {


    interface Event : Action


    }


    interface State : Contract


    #
    macaron-core/../contract/Contract.kt
    macaron-core/../contract/Contract.kt
    11

    View Slide

  15. #
    macaron-core/../components/Store.kt
    interface Store {


    val state: StateFlow


    val event: Flow


    fun dispatch(intent: I)


    fun process(event: A)


    fun dispose()


    fun collect(


    onState: (S) -> Unit,


    onEvent: (A?) -> Unit,


    ): Job


    }


    12

    View Slide

  16. 13
    macaron-core/../components/Processor.kt
    interface Processor {


    suspend fun process(intent: I, state: S): Flow


    }


    macaron-core/../components/Reducer.kt
    interface Reducer {


    suspend fun reduce(action: A, state: S): S


    }


    View Slide

  17. 14
    macaron-core/../components/DefaultStore.kt
    class DefaultStore(


    initialState: S,


    private val processor: Processor,


    private val reducer: Reducer,


    coroutineContext: CoroutineContext,


    ) : Store {


    private val scope: CoroutineScope = CoroutineScope(coroutineContext + SupervisorJob())


    private val intents: MutableSharedFlow = MutableSharedFlow(Int.MAX_VALUE, Int.MAX_VALUE)


    private val _state: MutableStateFlow = MutableStateFlow(initialState)


    private val _events: MutableStateFlow> = MutableStateFlow(emptyList())


    override val state: MutableStateFlow = _state


    override val events: Flow = _events.map { it.firstOrNull() as A? }


    override val currentState: S get() = _state.value


    override fun dispatch(intent: I) { scope.launch { intents.emit(intent) } }


    override fun process(event: A) {


    scope.launch { _events.emit(_events.value.filterNot { it == event }) }


    }


    private suspend fun send(event: Action.Event) { _events.emit(_events.value + event) }


    override fun dispose() { scope.cancel() }


    ...


    }


    View Slide

  18. 15
    macaron-core/../components/DefaultStore.kt
    override fun dispatch(intent:aI
    a
    )
    a
    {a


    scope.launch { intents.emit(intent)a}a


    a
    }a


    private val intents = MutableSharedFlow(...)

    View Slide

  19. #
    macaron-core/../components/DefaultStore.kt
    private val intents = MutableSharedFlow(...)


    init0{0


    scope.launch0{0


    intents0


    0 .flatMapMerge0{0intent0->0


    processor.process(intent,0currentState)0


    }0


    0 .map0{0action0->0


    if0(action0is0Action.Event)0send(action)0


    reducer.reduce(action,0currentState)


    }2


    .collect0{0state0->0_state.value0=0state }3


    }4


    }


    }


    15

    View Slide

  20. #
    macaron-core/../components/DefaultStore.kt
    intents0


    .flatMapMerge0{0intent0->0


    processor.process(intent,0currentState)0


    }0


    .map0{0action0->0


    if0(action0is0Action.Event)0send(action)0


    reducer.reduce(action,0currentState)


    }2


    .collect0{0state0->0_state.value0=0state }3
    15

    View Slide

  21. #
    macaron-core/../components/DefaultStore.kt
    intents0


    .flatMapMerge0{0intent0->0


    processor.process(intent,0currentState)0


    }0


    .map0{0action0->0


    if0(action0is0Action.Event)0send(action)0


    reducer.reduce(action,0currentState)


    }2


    .collect0{0state0->0_state.value0=0state }3
    15

    View Slide

  22. #
    macaron-core/../components/DefaultStore.kt
    intents0


    .flatMapMerge0{0intent0->0


    processor.process(intent,0currentState)0


    }0


    .map0{0action0->0


    if0(action0is0Action.Event)0send(action)0


    reducer.reduce(action,0currentState)


    }2


    .collect0{0state0->0_state.value0=0state }3


    15

    View Slide

  23. #
    macaron-core/../components/DefaultStore.kt
    intents0


    .flatMapMerge0{0intent0->0


    processor.process(intent,0currentState)0


    }0


    .map0{0action0->0


    if0(action0is0Action.Event)0send(action)0


    reducer.reduce(action,0currentState)


    }2


    .collect0{0state0->0_state.value0=0state }3


    15

    View Slide

  24. #
    macaron-core/../components/DefaultStore.kt
    intents0


    .flatMapMerge0{0intent0->0


    processor.process(intent,0currentState)0


    }0


    .map0{0action0->0


    if0(action0is0Action.Event)0send(action)0


    reducer.reduce(action,0currentState)


    }2


    .collect0{0state0->0_state.value0=0state }3


    private val _state = MutableStateFlow(initialState)


    override val state: StateFlow = _state


    15

    View Slide

  25. #
    macaron-core/../components/DefaultStore.kt
    intents0


    .flatMapMerge0{0intent0->0


    processor.process(intent,0currentState)0


    }0


    .map0{0action0->0


    if0(action0is0Action.Event)0send(action)0


    reducer.reduce(action,0currentState)


    }2


    .collect0{0state0->0_state.value0=0state }3


    private val _state = MutableStateFlow(initialState)


    override val state: StateFlow = _state


    15

    View Slide

  26. MVIΛ࢖ͬͯը໘Λ࣮૷͢Δ
    https://github.com/fika-tech/DroidKaigiSample
    発表資料とサンプルで使っている


    画像は著作権がフリーなものです。
    リンクは最後にまとめています。
    !

    View Slide

  27. 16
    ローディング リスト 初期エラー ページ


    ローディング
    ページエラー

    View Slide

  28. 17
    Domain
    Contract
    Processor
    Reducer

    View Slide

  29. 18
    Domain
    Contract
    Processor
    Reducer
    shared/../entities/Monster.kt
    data class Monster(


    val id: Int,


    val name: String,


    val imageUrl: String,


    )
    shared/../data/repositories/MonsterRepository.kt
    interface MonsterRepository {


    suspend fun getMonster(


    offset: Int,


    limit: Int,


    ): List


    }


    View Slide

  30. 19
    shared/../feature/monsterList/MonsterListState.kt
    sealed0class0MonsterListState0:0State0{0


    data0object0Initial0:0MonsterListState()0


    data0object0Loading0:0MonsterListState()0


    sealed class0Stable :0MonsterListState() {1


    abstract0val0monsterList:0List0


    data0class0List(


    override0val0monsterList:0List


    )0:0Stable()0


    data0class0PageLoading(


    override0val0monsterList:0List


    )0:0Stable()0


    data0class0PageError(


    override0val0monsterList:0List,0


    val error:0Throwable


    )0:0Stable()


    }0


    data0class0Error(val0error:0Throwable)0:0MonsterListState()0


    }1


    Domain
    Contract
    Processor
    Reducer

    View Slide

  31. shared/../feature/monsterList/MonsterListIntent.kt
    #
    sealed class MonsterListIntent : Intent {


    data object OnInit : MonsterListIntent()




    data class ClickItem(


    val monster: Monster


    ) : MonsterListIntent()




    data object ClickErrorRetry


    : MonsterListIntent()




    data object OnScrollToBottom


    : MonsterListIntent()


    }
    Domain
    Contract
    Processor
    Reducer
    20

    View Slide

  32. shared/../feature/monsterList/MonsterListAction.kt
    #
    sealed class MonsterListAction : Action {


    data object Loading : MonsterListAction()


    data class LoadSuccess(


    val monsterList: List


    ) : MonsterListAction()


    data class LoadError(


    val error: Throwable


    ) : MonsterListAction()


    data class NavigateDetails(


    val monster: Monster


    ) : MonsterListAction(), Action.Event


    }


    Domain
    Contract
    Processor
    Reducer
    21

    View Slide

  33. #
    shared/../feature/monsterList/MonsterProcessor.kt
    when (intent) {


    is MonsterListIntent.OnInit -> when (state) {


    is MonsterListState.Initial -> loadMonsterList(0, repository, ::emit)


    }


    is MonsterListIntent.ClickItem -> when (state) {


    is MonsterListState.Stable -> {


    emit(MonsterListAction.NavigateDetails(intent.monster))


    }


    }


    is MonsterListIntent.ClickErrorRetry -> when (state) {


    is MonsterListState.Error -> loadMonsterList(0, repository, ::emit)


    is MonsterListState.Stable.PageError ->


    loadMonsterList(state.currentOffset, repository, ::emit


    )


    }


    is MonsterListIntent.OnScrollToBottom -> when (state) {


    is MonsterListState.Stable.List ->


    loadMonsterList(state.currentOffset, repository, ::emit)


    }


    }
    Domain
    Contract
    Processor
    Reducer
    22

    View Slide

  34. #
    shared/../feature/monsterList/MonsterProcessor.kt
    private suspend fun loadMonsterList(a


    offset: Int,


    repository: Repository,


    emit: suspenda(MonsterListAction
    a
    )
    a
    -> Unit,


    a
    )a{a


    emit(MonsterListAction.Loading)


    runCatchinga{


    repository.getMonster(offset = offset, limit = 20)


    }.onSuccessa{ monsterLista->


    emit(MonsterListAction.LoadSuccess(monsterList))


    }.onFailurea{ errora->


    emit(MonsterListAction.LoadError(error))


    }


    }a
    Domain
    Contract
    Processor
    Reducer
    22

    View Slide

  35. #
    shared/../feature/monsterList/MonsterReducer.kt
    Domain
    Contract
    Processor
    Reducer
    when (action) {


    is MonsterListAction.Loading -> when (state) {


    is MonsterListState.Initial -> MonsterListState.Loading


    is MonsterListState.Stable.List ->


    MonsterListState.Stable.PageLoading(state.monsterList)


    }


    is MonsterListAction.LoadSuccess -> when (state) {


    is MonsterListState.Loading ->


    MonsterListState.Stable.List(action.monsterList)


    is MonsterListState.Stable.PageLoading ->


    MonsterListState.Stable.List(state.monsterList + action.monsterList)


    }


    is MonsterListAction.LoadError -> when (state) {


    is MonsterListState.Loading -> MonsterListState.Error(action.error)


    is MonsterListState.Stable.PageLoading ->


    MonsterListState.Stable.PageError(state.monsterList, action.error)


    }


    is MonsterListAction.NavigateDetails -> state


    }
    23

    View Slide

  36. 24
    Reducer
    Processor
    Store

    View Slide

  37. 25
    dispatch
    notify
    intent
    state
    Processor
    Reducer
    emit
    action
    Android / iOS
    send
    event
    Store

    View Slide

  38. 26 when (intent) {


    isaMonsterListIntent.OnInita-> {


    emit(MonsterListAction.Loading)


    runCatchinga{


    repository.getMonster(offset = 0, limit = 20)


    }.onSuccessa{ monsterLista->


    emit(MonsterListAction.LoadSuccess(monsterList))


    }


    ...


    }


    ...


    }
    when (action) {


    is MonsterListAction.Loading -> when (state) {


    is MonsterListState.Initial -> MonsterListState.Loading


    ...


    }


    is MonsterListAction.LoadSuccess -> when (state) {


    is MonsterListState.Loading ->


    MonsterListState.Stable.List(action.monsterList)


    ...


    }


    ...


    }
    // Processor
    // Reducer

    View Slide

  39. 27
    Reducer
    Processor
    Store

    View Slide

  40. ͢ͰʹεςʔτϚγϯ


    Λ࡞͍ͬͯΔʂ
    28

    View Slide

  41. MVIʹج͘StateMachine

    View Slide

  42. State Machineͱ͸ʁ
    #
    ݻମ
    on: ༥ղ


    transition: ӷମ
    ӷମ
    on: ڽݻ


    transition: ݸମ


    on: ؾԽ


    transition: ؾମ
    ؾମ
    on: ڽॖ(ӷԽ)


    transition: ӷମ
    29

    View Slide

  43. State Machineͱ͸ʁ
    #
    ݻମ
    on: ༥ղ


    transition: ӷମ


    on: ڽॖ


    transition: 🙅
    ӷମ
    on: ڽݻ


    transition: ݸମ


    on: ؾԽ


    transition: ؾମ
    ؾମ
    on: ڽॖ(ӷԽ)


    transition: ӷମ
    29

    View Slide

  44. # MVIΛجͮ͘StateMachine
    // Processor


    when (intent) {


    is MonsterListIntent -> when (state) {


    is MonsterListState -> { ... }


    }


    }


    // Processor


    when (state) {


    is MonsterListState -> when (intent) {


    is MonsterListIntent -> { ... }


    }


    }


    // Reducer


    when (action) {


    is MonsterAction -> when (state) {


    is MonsterListState -> { ... }


    }


    }


    // Reducer


    when (state) {


    is MonsterListState -> when (action) {


    is MonsterListAction -> { ... }


    }


    }


    30

    View Slide

  45. 31
    shared/../feature/monsterList/MonsterProcessor.kt
    // Processor


    when (intent) {


    is MonsterListIntent.OnInit -> when (state) {


    is MonsterListState.Initial -> loadMonsterList(0, repository, ::emit)


    }


    ...


    }


    // Reducer


    when (action) {


    is MonsterListAction.Loading -> when (state) {


    is MonsterListState.Initial -> MonsterListState.Loading


    ...


    }


    ...


    }


    View Slide

  46. #
    shared/../feature/monsterList/MonsterProcessor.kt
    // Processor


    when (state) {


    is MonsterListState.Initial -> when (intent) {


    is MonsterListIntent.OnInit -> loadMonsterList(0, repository, ::emit)


    }


    ...


    }


    // Reducer


    when (state) {


    is MonsterListState.Initial -> when (action) {


    is MonsterListAction.Loading -> MonsterListState.Loading


    ...


    }


    ...


    }
    31

    View Slide

  47. 32


    Initial
    intent: OnInit


    action: Loading


    reduce: State.Loading


    action: LoadSuccess|LoadError
    // Processor


    when (state) {


    is MonsterListState.Initial -> when (intent) {


    is MonsterListIntent.OnInit -> {


    emit(MonsterListAction.Loading)


    ...


    emit(MonsterListAction.LoadSuccess)


    // or


    emit(MonsterListAction.LoadError)


    }


    }


    ...


    }


    // Reducer


    when (state) {


    is MonsterListState.Initial -> when (action) {


    is MonsterListAction.Loading -> MonsterListState.Loading


    ...


    }


    ...


    }

    View Slide

  48. # Initial
    intent: OnInit


    action: Loading


    reduce: State.Loading


    action: LoadSuccess|LoadError
    Stable
    intent: ClickItem


    event: NavigateDetails


    Error
    intent: OnInit


    action: Loading


    reduce: State.Loading


    action: LoadSuccess | LoadError
    Initial
    intent: OnInit


    action: Loading


    reduce: State.Loading


    action: LoadSuccess | LoadError
    Loading
    receive: LoadSuccess


    reduce: State.Stable.List


    receive: LoadError


    reduce: State.Stable.Error
    Error
    intent: ClickErrorRetry


    action: Loading


    reduce: State.Stable.Loading


    action: LoadSuccess | LoadError
    Loading
    receive: LoadSuccess


    reduce: State.Stable.List


    receive: LoadError


    reduce: State.Stable.Error
    ݻମ
    on: ༥ղ


    transition: ӷମ
    ӷମ
    on: ڽݻ


    transition: ݸମ


    on: ؾԽ


    transition: ؾମ
    ؾମ
    on: ڽॖ(ӷԽ)


    transition: ӷମ
    33

    View Slide

  49. # Initial
    intent: OnInit


    action: Loading


    reduce: State.Loading


    action: LoadSuccess|LoadError
    Stable
    intent: ClickItem


    event: NavigateDetails


    Error
    intent: OnInit


    action: Loading


    reduce: State.Loading


    action: LoadSuccess | LoadError
    Initial
    intent: OnInit


    action: Loading


    reduce: State.Loading


    action: LoadSuccess | LoadError
    Loading
    receive: LoadSuccess


    reduce: State.Stable.List


    receive: LoadError


    reduce: State.Stable.Error
    Error
    intent: ClickErrorRetry


    action: Loading


    reduce: State.Stable.Loading


    action: LoadSuccess | LoadError
    Loading
    receive: LoadSuccess


    reduce: State.Stable.List


    receive: LoadError


    reduce: State.Stable.Error
    33

    View Slide

  50. Initial
    intent: OnInit


    action: Loading


    reduce: State.Loading


    action: LoadSuccess|LoadError


    intent: ClickErrorRetry


    action: 🙅


    #
    Stable
    intent: ClickItem


    event: NavigateDetails


    Error
    intent: OnInit


    action: Loading


    reduce: State.Loading


    action: LoadSuccess | LoadError
    Initial
    intent: OnInit


    action: Loading


    reduce: State.Loading


    action: LoadSuccess | LoadError
    Loading
    receive: LoadSuccess


    reduce: State.Stable.List


    receive: LoadError


    reduce: State.Stable.Error
    Error
    intent: ClickErrorRetry


    action: Loading


    reduce: State.Stable.Loading


    action: LoadSuccess | LoadError
    Loading
    receive: LoadSuccess


    reduce: State.Stable.List


    receive: LoadError


    reduce: State.Stable.Error
    33

    View Slide

  51. 34
    MonsterListState
    Stable
    I.ClickMonsterEntry > E.NavigateDetails
    Initial
    I.OnInit > A.Loading, [A.LoadSuccess | A.LoadError]
    Loading
    Error
    I.ClickErrorRetry > A.Loading, [A.LoadSuccess | A.LoadError]
    Stable.Initial
    I.OnScrollToBottom > A.Loading, [A.LoadSuccess | A.LoadError]
    Stable.PageLoading
    Stable.PageError
    I.ClickErrorRetry > A.Loading, [A.LoadSuccess | A.LoadError]
    A.Loading
    A.LoadSuccess
    A.LoadError A.Loading
    A.Loading A.LoadSuccess
    A.LoadError A.Loading
    PlantUML Diagram

    View Slide

  52. Jetpack Compose & SwiftUIͷಋೖ

    View Slide

  53. 34
    Figma ࢓༷ॻ UMLਤ

    View Slide

  54. 35
    androidApp/../ui/screen/MonsterListScreen.kt
    @Composable


    fun MonsterListScreen(


    contract: Contract<...>,


    ) {


    LaunchedEffect(Unit) {


    contract.dispatch(MonsterListIntent.OnInit)


    }


    contract.handleEvents { action ->


    when (action) {


    is MonsterListAction.NavigateDetails ->


    Timber.d("Handle Navigation")


    else -> Unit


    }


    }


    MonsterListContent(contract.state, contract.dispatch)


    }

    View Slide

  55. 36
    androidApp/../ui/screen/MonsterListScreen.kt
    state.rendera{ ... }


    state.rendera{


    val lazyListState = rememberLazyListState().apply {


    onScrolledToBottom { dispatch(MonsterListIntent.OnScrollToBottom) }


    }


    LazyColumn(state = lazyListState, modifier = Modifier.fillMaxSize()) {


    items(items = monsterList) { monster ->


    MonsterListItem(name = monster.name, imageUrl = monster.imageUrl,


    onClick = { dispatch(MonsterListIntent.ClickItem(monster = monster)) })


    }


    state.renderItemsa{


    item { PageLoadingIndicatorItem() }


    }


    state.renderItemsa{


    item {


    PageErrorItem(error = it.error,


    onClickRetry = { dispatch(MonsterListIntent.ClickErrorRetry) })


    }


    }


    }


    }


    }


    View Slide

  56. #
    androidApp/../ui/screen/MonsterListScreen.kt
    state.rendera{ LoadingIndicator() }


    state.rendera{ ... }


    state.rendera{ ... }
    36

    View Slide

  57. 37
    androidApp/../ui/screen/MonsterListScreen.kt
    @Composable


    fun MonsterListScreen(


    contract: Contract<...>,


    ) {


    LaunchedEffect(Unit) {


    contract.dispatch(MonsterListIntent.OnInit)


    }


    contract.handleEvents { action ->


    when (action) {


    is MonsterListAction.NavigateDetails ->


    Timber.d("Handle Navigation")


    else -> Unit


    }


    }


    MonsterListContent(contract.state, contract.dispatch)


    }

    View Slide

  58. 38
    androidApp/../core/Contract.kt
    data class Contract(


    val state: S,


    val dispatch: (I) -> Unit = {},


    val event: A? = null,


    val process: (A) -> Unit = {},


    )


    @Composable


    fun contract(


    store: Store,


    ): Contract {


    val state by store.state.collectAsState()


    val event by store.event.collectAsState(initial = null)


    return Contract(state, event, store::dispatch, store::process)


    }


    View Slide

  59. 39
    iosApp/../Core/Contract.swift
    open class Contract: ObservableObject {


    @Published public private(set) var state: S


    public let dispatch: (I) -> Void


    public var events: some Publisher { eventSubject }


    private let eventSubject = PassthroughSubject()


    private var cancellables: Set = []


    public init(store: Store) {


    self.state = store.currentState as! S


    self.dispatch = { store.dispatch(intent: $0) }


    AnyCancellable { store.dispose() }.store(in: &cancellables)


    store.collect { [weak self] newState in


    if let self, let newState = newState as? S {


    self.state = newState


    }


    } onEvent: { [eventSubject, store] event in


    if let action = event as? A {


    store.process(event: action)


    eventSubject.send(action)


    }


    }


    }


    }

    View Slide

  60. 40
    iosApp/../UI/Screen/MonsterListScreen.swift
    struct MonsterListScreen: View {


    @ObservedObject


    var contract: Contract




    var body: some View {


    ZStack {


    switch contract.state {


    case _ as MonsterListState.Loading: LoadingIndicator()


    case let state as MonsterListState.Stable:


    MonsterListView(state: state, dispatch: contract.dispatch)


    case let state as MonsterListState.Error:


    FullScreenErrorView(


    error: state.error,


    onClickRetry: {


    contract.dispatch(MonsterListIntent.ClickErrorRetry())


    }


    )


    default: EmptyView()


    }


    }.onAppear { contract.dispatch(.OnInit()) }


    }


    }

    View Slide

  61. 40
    ローディング リスト 初期エラー ページ


    ローディング
    ページエラー

    View Slide

  62. StateMachine DSL

    View Slide

  63. 41


    whenจ͕ଟ͍ɹ
    1
    ▶︎
    s
    t a
    t e

    i n t e n t

    a
    c
    t i o n
    増えていくと


    w h e n
    文も二次関数的に増えていく 😱
    ॲཧ͕ผΕ͍ͯΔ
    2
    ▶︎コードの流れ分かりにくい


    ▶︎バグ修正や追加開発難しくなる

    View Slide

  64. 42
    PlantUML Diagram state {


    process {


    emit(MonsterListAction.Hoge)


    emit(MonsterListAction.Hoge)


    }


    reduce {


    MonsterListState.Hoge


    }


    }


    View Slide

  65. 43
    StateMachine.kt → DSLΛ௨ͯ͠UMLΛMapܕʹอ࣋͢Δ
    StateMachineProcessor.kt →


    StateMachineͰ࡞ΒΕͨMapΛ࢖ͬͯProcessॲཧΛ൑அ͢Δ
    StateMachineReducer.kt →


    StateMachineͰ࡞ΒΕͨMapΛ࢖ͬͯReduceॲཧΛ൑அ͢Δ
    ref: https://github.com/fika-tech/Macaron/tree/main/macaron-statemachine

    View Slide

  66. // Processor


    is MonsterListState.Initial ->


    when (intent) {


    is MonsterListIntent.OnInit -> {


    loadMonsterList(0, repository, ::emit)


    }


    }
    # state {


    process {


    loadMonsterList(0, repository, ::emit)


    }


    reduce {




    }


    }a
    // Reducer


    is MonsterListState.Initial ->


    when (action) {


    is MonsterListAction.Loading -> {


    MonsterListState.Loading


    }


    }
    MonsterListState
    Initial
    I.OnInit > A.Loading, [A.LoadSuccess | A.LoadError]
    Loading
    A.Loading
    44

    View Slide

  67. # state {


    process {


    loadMonsterList(0, repository, ::emit)


    }


    reduce {


    MonsterListState.Loading


    }


    }a
    MonsterListState
    Initial
    I.OnInit > A.Loading, [A.LoadSuccess | A.LoadError]
    Loading
    A.Loading
    // Reducer


    is MonsterListState.Initial ->


    when (action) {


    is MonsterListAction.Loading -> {


    MonsterListState.Loading


    }


    }
    44

    View Slide

  68. 45
    class MonsterListStateMachine(private val repository: MonsterRepository)


    : StateMachine(


    builder = {


    state {


    process { loadMonsterList(0, repository, ::emit) }


    reduce { MonsterListState.Loading }


    }


    state {


    reduce { MonsterListState.Stable.List(action.monsterList) }


    reduce { MonsterListState.Error(action.error) }


    }


    state {


    process { emit(MonsterListAction.NavigateDetails(intent.monster)) }


    }


    state {


    process { loadMonsterList(state.currentOffset, repository, ::emit) }


    reduce { MonsterListState.Stable.PageLoading(state.monsterList) }


    }


    state {


    reduce { MonsterListState.Stable.List(state.monsterList + action.monsterList) }


    reduce { MonsterListState.Stable.PageError(state.monsterList, action.error) }


    }


    state {


    process { loadMonsterList(state.currentOffset, repository, ::emit) }


    reduce { MonsterListState.Stable.PageLoading(state.monsterList) }


    }


    state {


    process { loadMonsterList(0, repository, ::emit) }


    reduce { MonsterListState.Loading }


    }


    }


    )

    View Slide

  69. 46
    val stateMachine =
    MonsterListStateMachine(monsterRepository = ...)


    val processor = StateMachineProcessor(stateMachine)


    val reducer = StateMachineReducer(stateMachine)
    Reducer
    Processor
    Store

    View Slide

  70. 47


    whenจ͕ଟ͍ɹ
    1
    ▶︎
    w h e n
    文一個も必要なくなった


    ▶︎書くコードもかなり減った
    ॲཧ͕ผΕ͍ͯΔ
    2
    ▶︎
    p
    r
    o c
    e s
    s
    o r

    r
    e d
    u c
    e r
    の処理は同じ
    ブロックに入っているので、コードの
    流れわかりやすくなった

    View Slide

  71. ·ͱΊ

    View Slide

  72. #
    ͍ΖΜͳٕज़Ϩϕϧͷϝϯόʔ͕Δ
    48


    View Slide

  73. #
    2
    ٕज़΋֮͑ͳ
    ͍ͱ͍͚ͳ͍
    1
    Ҋ݅ͷυϝΠϯ
    ஌ࣝ΋֮͑ͳ͍
    ͱ͍͚ͳ͍
    49


    View Slide

  74. #
    2
    ٕज़΋֮͑ͳ
    ͍ͱ͍͚ͳ͍
    1
    Ҋ݅ͷυϝΠϯ
    ஌ࣝ΋֮͑ͳ͍
    ͱ͍͚ͳ͍
    KMPͰٵऩ ·ͣ͸
    Compose΍
    SwiftUIʹूத
    49


    View Slide

  75. #
    Compose &
    SwiftUI
    ݖݶͷऔಘ
    PUSH௨஌
    50


    View Slide

  76. UML͕࢓༷ॻʹͳΔ
    1
    ▶︎
    U
    M L
    を見ながら
    S t a
    t e M a
    c
    h i n e
    も��
    実装しやすい
    ࣮૷͸ಉ࣌ฒߦͰ͖Δ
    2
    ▶︎ 最初に
    U
    M L

    S t a
    t e

    E n t i t y
    を用意
    するだけで、アプリの画面も
    K M P
    側の
    S t a
    t e M a
    c
    h i n e
    も同時に実装できる
    Kotlinͷ஌ࣝগͳͯ͘΋͍͍
    3
    ▶︎
    S t a
    t e M a
    c
    h i n e

    D S L
    化されている��
    ので
    i O S
    エンジニアでも複雑すぎない
    画面の
    S t a
    t e M a
    c
    h i n e
    を書けます
    খ͍͞࢓༷มߋʹ΋
    ରԠ͠΍͍͢
    4
    ▶︎ボタンなど追加する場合、
    I n t e n t

    A
    c
    t i o n
    を用意して
    p
    r
    o c
    e s
    s

    r
    e d
    u c
    e

    書くだけで追加できます
    51


    View Slide

  77. 52


    ਂ͍Domain஌͕ࣝඞཁ
    2
    ▶︎
    U
    M L
    を作る時に
    D o m
    a
    i n

    U
    I

    ロジックを落とし込むので
    D o m
    a
    i n
    層や
    U
    I
    の動きの深い理解が必要
    ྆OSͷ஌͕ࣝ๬·͍͠
    3
    ▶︎
    U
    M L
    を作成する時に両
    O S
    の動きを
    考えながら組む事が必要
    ֶशίετ͕͋Δ
    1
    ▶︎
    M V
    I

    S t a
    t e M a
    c
    h i n e
    の考え方が
    慣れていない方が多い
    େ͖͍࢓༷มߋʹऑ͍
    4
    ▶︎仕様変更が大きければ、
    S t a
    t e
    の�
    構造を考え直すことがあります


    View Slide

  78. Macaronʹ͍ͭͯ
    53


    0.1.0ϦϦʔε
    1
    ▶︎ドキュメンテーションまだ用意できてない


    ▶︎
    A
    P I
    はこれから変わる可能性ある
    Roadmap
    2
    ▶︎ ドキュメンテーションの作成


    ▶︎
    M i d
    d
    l e w a
    r
    e / P l u g i n
    の仕組みを考え直す


    ▶︎ サンプルコードやアプリを増やす


    ▶︎
    U
    M L
    からのコード生成


    ▶︎
    T
    i m
    e T
    r
    a
    v
    e l
    デバッグ
    https://github.com/fika-tech/Macaron

    View Slide

  79. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠

    View Slide

  80. 54
    Attributions:


    ▶︎
    https://icons8.com/icons/collections/JLYaFSTqBIyi


    ▶︎
    https://www.flaticon.com/free-icons/flow-chart


    ▶︎
    https://www.flaticon.com/free-icon/uml_5687240


    ▶︎
    https://www.flaticon.com/free-icons/feet


    ▶︎
    https://greenchess.net/


    ▶︎
    https://www.freepik.com/free-vector/scary-monster-
    set-colored-monsters-with-teeth-eyes-illustration-
    funny-monsters_13031939.htm


    ▶︎
    https://www.freepik.com/free-vector/monsters-set-
    cartoon-cute-character-isolated-white-
    background_13031453.htm
    References:


    ▶︎
    https://github.com/Tinder/StateMachine


    ▶︎
    https://github.com/orbit-mvi/orbit-mvi


    ▶︎
    https://github.com/arkivanov/MVIKotlin


    ▶︎
    https://github.com/reduxjs/redux


    ▶︎
    https://github.com/ReactorKit/ReactorKit


    ▶︎
    https://github.com/kubode/reaktor
    Resources:


    ▶︎
    https://developer.android.com/topic/architecture/ui-layer/state-production


    ▶︎
    https://medium.com/swlh/mvi-architecture-with-android-fcde123e3c4a


    ▶︎
    https://yuyakaido.hatenablog.com/entry/2017/12/12/235143


    ▶︎
    https://plantuml.com/
    Links:


    ▶︎
    https://github.com/fika-tech/Macaron


    ▶︎
    https://github.com/fika-tech/DroigKaigi-Sample

    View Slide