Slide 1

Slide 1 text

MVIʹجͮ͘StateMachineΞʔΩςΫνϟ Kotlin Multiplatform & Jetpack Compose & SwiftUI Λ૊Έ߹ΘͤΔ DroidKaigi 2023 マルコ&そば屋 1

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

ςΫϊϩδʔͷ঺հ 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

Slide 5

Slide 5 text

ςΫϊϩδʔͷ঺հ

Slide 6

Slide 6 text

5 ςΫϊϩδʔͷ঺հ

Slide 7

Slide 7 text

6 ςΫϊϩδʔͷ঺հ ▶︎ コードを削減 ▶︎ 直感的 ▶︎ 開発を加速させる ▶︎ パワフル ▶︎ 高度なアニメーション制御 ▶︎ シンプルになったデータフロー ▶︎ A P I の拡 ▶︎ 新タイプのグラフとインタラクティブな機能 Jetpack Compose SwiftUI

Slide 8

Slide 8 text

MVIΞʔΩςΫνϟͷ঺հ

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

10 MVIͷ঺հ Immutability 1 Unidirectional Data Flow 2

Slide 13

Slide 13 text

MVIΞʔΩςΫνϟͷ࣮૷ https://github.com/fika-tech/Macaron スライドに収めるために正しいコード スタイル使っていません !

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

# 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

Slide 16

Slide 16 text

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 }

Slide 18

Slide 18 text

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(...)

Slide 19

Slide 19 text

# 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

Slide 20

Slide 20 text

# 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

Slide 21

Slide 21 text

# 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

Slide 22

Slide 22 text

# 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

Slide 23

Slide 23 text

# 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

Slide 24

Slide 24 text

# 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

Slide 25

Slide 25 text

# 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

Slide 26

Slide 26 text

MVIΛ࢖ͬͯը໘Λ࣮૷͢Δ https://github.com/fika-tech/DroidKaigiSample 発表資料とサンプルで使っている 画像は著作権がフリーなものです。 リンクは最後にまとめています。 !

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

17 Domain Contract Processor Reducer

Slide 29

Slide 29 text

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 }

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

# 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

Slide 34

Slide 34 text

# 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

Slide 35

Slide 35 text

# 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

Slide 36

Slide 36 text

24 Reducer Processor Store

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

27 Reducer Processor Store

Slide 40

Slide 40 text

͢ͰʹεςʔτϚγϯ Λ࡞͍ͬͯΔʂ 28

Slide 41

Slide 41 text

MVIʹج͘StateMachine

Slide 42

Slide 42 text

State Machineͱ͸ʁ # ݻମ on: ༥ղ transition: ӷମ ӷମ on: ڽݻ transition: ݸମ on: ؾԽ transition: ؾମ ؾମ on: ڽॖ(ӷԽ) transition: ӷମ 29

Slide 43

Slide 43 text

State Machineͱ͸ʁ # ݻମ on: ༥ղ transition: ӷମ on: ڽॖ transition: 🙅 ӷମ on: ڽݻ transition: ݸମ on: ؾԽ transition: ؾମ ؾମ on: ڽॖ(ӷԽ) transition: ӷମ 29

Slide 44

Slide 44 text

# 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

Slide 45

Slide 45 text

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 ... } ... }

Slide 46

Slide 46 text

# 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

Slide 47

Slide 47 text

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 ... } ... }

Slide 48

Slide 48 text

# 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

Slide 49

Slide 49 text

# 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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Jetpack Compose & SwiftUIͷಋೖ

Slide 53

Slide 53 text

34 Figma ࢓༷ॻ UMLਤ

Slide 54

Slide 54 text

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) }

Slide 55

Slide 55 text

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) }) } } } } }

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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) }

Slide 58

Slide 58 text

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) }

Slide 60

Slide 60 text

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()) } } }

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

StateMachine DSL

Slide 63

Slide 63 text

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 ▶︎コードの流れ分かりにくい ▶︎バグ修正や追加開発難しくなる

Slide 64

Slide 64 text

42 PlantUML Diagram state { process { emit(MonsterListAction.Hoge) emit(MonsterListAction.Hoge) } reduce { MonsterListState.Hoge } }

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

// 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

Slide 67

Slide 67 text

# 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

Slide 68

Slide 68 text

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 } } } )

Slide 69

Slide 69 text

46 val stateMachine = MonsterListStateMachine(monsterRepository = ...) val processor = StateMachineProcessor(stateMachine) val reducer = StateMachineReducer(stateMachine) Reducer Processor Store

Slide 70

Slide 70 text

47 whenจ͕ଟ͍ɹ 1 ▶︎ w h e n 文一個も必要なくなった ▶︎書くコードもかなり減った ॲཧ͕ผΕ͍ͯΔ 2 ▶︎ p r o c e s s o r と r e d u c e r の処理は同じ ブロックに入っているので、コードの 流れわかりやすくなった

Slide 71

Slide 71 text

·ͱΊ

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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 の� 構造を考え直すことがあります

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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