ã¯ããã« ããã«ã¡ã¯ãã°ããŒãã«ãããã¯ãéçºæ¬éš ã°ããŒãã«ã¢ããªéš ã¢ããªåºç€ãããã¯ã®æ¡å·ã§ããæ®æ®µã¯ZOZOFITã»ZOZOMETRYãªã©ã®èšæž¬ã¢ããªã®Androidéçºã«æºãã£ãŠããŸããæ¬èšäºã§ã¯ZOZOFITã®Androidã¢ããªã§åãçµãã MVVMããMVIãžã®ç§»è¡ãšãç¬èªMVIã©ã€ãã©ãªã®éçºã«ã€ããŠç޹ä»ããŸãããªããç¬èªMVIã©ã€ãã©ãªã䜿ã£ãMVIã¢ãŒããã¯ãã£ãžã®ç§»è¡ã¯2024幎9æã«éå§ããŸããã ç®æ¬¡ ã¯ããã« ç®æ¬¡ çšèª ZOZOFIT MVVM SSOT UDF MVI ç§ãã¡ã®MVVMã¢ãŒããã¯ãã£ã®åé¡ç¹ ViewModelã§ã®State管çãè€éã« ViewãšViewModelã®è²¬åãææ§ã« ã€ãã³ãéç¥ãšç»é¢é·ç§»ã®äžçµ±äž ç§ãã¡ã®MVVMã¢ãŒããã¯ãã£ã®æ¹åæ¹é UiStateã«ããState管çã®åçŽå ãŠãŒã¶ãŒæäœããšã®ã¡ãœããå®çŸ©ã«ãã責åã®æç¢ºå Channelã«ããã€ãã³ãéç¥ãšç»é¢é·ç§»ã®çµ±äž ç§ãã¡ã®MVVMã¢ãŒããã¯ãã£ã®æ¹åæ¹éãéçšã§ããã MVIã¢ãŒããã¯ãã£ã®å°å
¥ãšç¬èªã©ã€ãã©ãªã®äœæ ããŒã¿ãã㌠å®è£
ã€ã³ã¿ãã§ãŒã¹ã®å®çŸ© ç§»è²ãçšããã€ã³ã¿ãã§ãŒã¹ã®å®è£
MVIã¢ãŒããã¯ãã£ãç¬èªMVIã©ã€ãã©ãªã§å®è£
ãã Contract: Stateã»Actionã»SideEffectã®å®çŸ© ViewModel: Actionã®åŠçãšStateæŽæ° View: MviContentã«ããCompose飿º ãã¹ã: Actionãéä¿¡ããŠStateã»SideEffectãæ€èšŒ MVVMã¢ãŒããã¯ãã£ããMVIã¢ãŒããã¯ãã£ã«ç§»è¡ããŠã¿ãŠ ããŒã å
šäœã§äžè²«ããå®è£
ãã§ããããã«ãªã£ã PRã¬ãã¥ãŒã®è³ªãåäžãã AIã³ãŒãã£ã³ã°ãšãŒãžã§ã³ããšã®åæ¥ããããããªã£ã ãŸãšã çšèª ãŸããæ¬èšäºã§äœ¿çšããçšèªãæŽçããŸãã ZOZOFIT ZOZOFITã¯ãèªå®
ã§æè»œã«é«ç²ŸåºŠãª3Dããã£ã¹ãã£ã³ãã§ããäœå管çãµãŒãã¹ã§ããZOZOSUITãšå°çšã¹ããŒããã©ã³ã¢ããªã掻çšããå
šèº«3Dã¹ãã£ã³ãå¯èœã§ããèšæž¬ããŒã¿ã«åºã¥ããäœã®å€åã3Dã¢ãã«ãšæ°å€ã§å¯èŠåã§ããŸããæ é€çŽ ãèšé²ã»åæããããŒããžã£ãŒãã«æ©èœãªã©ãèšæž¬ä»¥å€ã®æ©èœã§ãç·åçãªå¥åº·ç®¡çããµããŒãããŠããŸããæ¬èšäºã§ã¯ã¢ã¡ãªã«ãªã©æµ·å€ã§å±éããŠããZOZOFITã®Androidã¢ããªã§ã®æ¹åã«ã€ããŠã話ãããŸãã zozofit.com MVVM MVVMïŒModel-View-ViewModelïŒã¯ãUIã®ç¶æ
ã管çããã¢ãŒããã¯ãã£ã¹ã¿ã€ã«ã®1ã€ã§ããModelã»Viewã»ViewModelã®3èŠçŽ ã§æ§æãããViewModelãModelãšViewã®ä»²ä»åœ¹ãæ
ããŸããViewã¯ViewModelãå
¬éããç¶æ
ãç£èŠããŠç»é¢ã«åæ ãããŠãŒã¶ãŒæäœã¯ViewModelã®ã¡ãœãããåŒã³åºãããšã§åŠçãããŸããAndroidã¢ããªéçºã§åºãæ¡çšãããŠããã¢ãŒããã¯ãã£ã§ããããŒã¿ã®æµãã¯æ¬¡ã®ãšããã§ãã ViewããŠãŒã¶ãŒæäœãViewModelã®ã¡ãœããåŒã³åºããšããŠéã ViewModelãç¶æ
ãæŽæ°ããStateFlowã§å
¬éãã ViewãStateFlowã賌èªããŠç»é¢ã«åæ ãã SSOT SSOTïŒSingle Source of TruthïŒã¯ãåããŒã¿åã«å¯ŸããŠå¯äžã®ä¿¡é Œã§ããããŒã¿ãœãŒã¹ãæã€èãæ¹ã§ããSSOTã ããããŒã¿ã倿Žã§ããäžå€ã®åã§å
¬éããŸããããã«ããããŒã¿ã®å€æŽã1ç®æã«éçŽãããä»ã®åã«ããæ¹ãããé²ãããã°ã®è¿œè·¡ã容æã«ããŸãã UDF UDFïŒUnidirectional Data FlowïŒã¯ãSSOTãšçµã¿åãããŠäœ¿çšããããã¿ãŒã³ã§ããç¶æ
ïŒããŒã¿ïŒã¯äžäœããäžäœãžäžæ¹åã«æµããç¶æ
ã倿Žããã€ãã³ãã¯ãã®éæ¹åã«æµããŸããå
·äœçã«ã¯æ¬¡ã®æµãã§ããŒã¿ãæŽæ°ãããŸãã Androidå
¬åŒããã¥ã¡ã³ã ã§ããå
ç¢ãªã¢ãŒããã¯ãã£ã®ååãšããŠSSOTãšUDFã瀺ãããŠããŸãããã®2ã€ãã»ããã§å®ãããšã§ãããŒã¿ã®æŽåæ§ãä¿ããããããã°ã»ãã¹ãã»ã¬ãã¥ãŒããããããªããŸããæ¬èšäºã§ç޹ä»ããMVIã¢ãŒããã¯ãã£ããã®ååã«åºã¥ããŠãããSSOTãšUDFã®çè§£ãå¿
èŠã§ãã ãŠãŒã¶ãŒæäœïŒãã¿ã³æŒäžãªã©ïŒãäžäœã¹ã³ãŒãã§çºçãã ã€ãã³ããäžäœã¹ã³ãŒãããäžäœã¹ã³ãŒãïŒSSOTïŒãžåãã£ãŠæµãã SSOTã§ããŒã¿ã倿Žãããäžå€ã®åãšããŠå
¬éããã 倿Žãããç¶æ
ãäžäœã¹ã³ãŒãããäžäœã¹ã³ãŒããžæµãã äžäœã¹ã³ãŒããæ°ããç¶æ
ãåãåããè¡šç€ºãæŽæ°ãã MVI MVIïŒModel-View-IntentïŒã¯ãUDFã®ååã«åºã¥ããŠUIã®ç¶æ
ã管çããã¢ãŒããã¯ãã£ã¹ã¿ã€ã«ã®1ã€ã§ããããŒã¿ã®æµããäžæ¹åã«åºå®ããããããç¶æ
倿Žã®èµ·ç¹ãšçµæã远跡ãããããªããŸããMVIã®ååã¯Modelã»Viewã»Intentã®é æåã«ç±æ¥ããŠããã以äžã®3èŠçŽ ã§æ§æãããŸãããªããæ¬èšäºã§ã¯çšèªã®çŽãããããé¿ããããã以éModelãStateãIntentãActionãšåŒã³ãŸãã èŠçŽ åœ¹å² Model(State) ç»é¢ã®çŸåšç¶æ
ã衚ãããŒã¿ãUIã¯ãã®å€ã®ã¿ããæ§ç¯ãããã View Stateãåãåã£ãŠç»é¢ã«åæ ãããŠãŒã¶ãŒæäœãActionãšããŠçºè¡ããã Intent(Action) ãŠãŒã¶ãŒæäœãå€éšã€ãã³ããªã©ãç¶æ
æŽæ°ã®ãã£ãããšãªãå
¥åã ViewããŠãŒã¶ãŒã®æäœãActionãšããŠçºè¡ãã Actionãããšã«StateãæŽæ°ããã æŽæ°ãããStateãViewãžéç¥ãããç»é¢ã«åæ ããã ç§ãã¡ã®MVVMã¢ãŒããã¯ãã£ã®åé¡ç¹ ZOZOFITã®Androidã¢ããªã¯2022幎ã®ãªãªãŒã¹åœåããJetpack Composeãæ¡çšããŠãããåœæããMVVMã¢ãŒããã¯ãã£ãæ¡çšããŠéçºãç¶ããŠããŸãããç§ãã¡ã®MVVMã¢ãŒããã¯ãã£ã§ã¯ViewModelã§å®çŸ©ããStateFlowãViewã§è³ŒèªããViewModelã®ã¡ãœãããViewããåŒã³åºããŠç¶æ
ãæŽæ°ããããšããã·ã³ãã«ãªèšèšã§ããã class CounterViewModel : ViewModel() { private val _counter = MutableStateFlow( 0 ) val counter: StateFlow< Int > = _counter.asStateFlow() fun increment() { _counter.value + = 1 } fun decrement() { _counter.value - = 1 } fun reset() { _counter.value = 0 } } ãããéçºãé²ã¿ç»é¢æ°ãæ©èœãå¢ããã«ã€ããŠãJetpack ComposeãšMVVMã®çµã¿åããã«ãããŠãããã€ãã®åé¡ãé¡åšåããŠãããŸãããç¹ã«StateFlowã®ç®¡çãã€ãã³ãéç¥ã®èšèšãããŒã å
ã§çµ±äžãããŠããããäžå
·åãã¬ãã¥ãŒè² è·ã®å¢å ã«ã€ãªãã£ãŠããŸãããå
·äœçã«ã¯ä»¥äžã®ãããªèª²é¡ããããŸããã ViewModelã§ã®State管çãè€é㫠衚瀺ããŒã¿ããšã«åå¥ã®StateFlowãå®çŸ©ããŠãããããç»é¢ãè€éã«ãªãã»ã© Flow.map ã combine ã«ããåæãå¢ããŠãããŸãããåFlowã®æŽæ°ã¿ã€ãã³ã°ãææ¡ãã¥ãããªããæå³ããªãåComposeãç»é¢ã®ãã©ã€ããçºçããŠããŸããã // CounterViewModel.kt: 衚瀺ããŒã¿ããšã«åå¥ã®Flowãå®çŸ©ãããŠãã class CounterViewModel : ViewModel() { private val _counter = MutableStateFlow( 0 ) val counter: StateFlow< Int > = _counter.asStateFlow() // Flow.mapã§æŽŸçStateFlowãäœæ â æŽæ°ã¿ã€ãã³ã°ãåããã«ãã val doubleCount: StateFlow< Int > = _counter.map { it * 2 } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 0 ) val tripleCount: StateFlow< Int > = _counter.map { it * 3 } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 0 ) } ãŸãViewåŽã®Composable颿°ã§ãåŒæ°ãå¢ããŠããåŸåããããŸãããViewåŽã®ã³ãŒãã«å€ãã® collectAsState ãå®çŸ©ãããèŠéããæªãã管çãé£ããã³ãŒãã«ãªãããšãå€ã
ãããŸããã // CounterScreen.kt: Flowããšã«åå¥ã«collectããåŒæ°ãå¢ããŠãã @Composable fun CounterScreen(viewModel: CounterViewModel, navController: NavController) { val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() val counter by viewModel.counter.collectAsStateWithLifecycle() val doubleCount by viewModel.doubleCount.collectAsStateWithLifecycle() val tripleCount by viewModel.tripleCount.collectAsStateWithLifecycle() CounterScreenContent( isLoading = isLoading, counter = counter, doubleCount = doubleCount, tripleCount = tripleCount, onIncrement = { /* ... */ }, onDecrement = { /* ... */ }, onReset = viewModel :: reset, // ... ) } ViewãšViewModelã®è²¬åãææ§ã« ViewãViewModelã®æ§é ãç¥ããããã³ãŒãã«ãªããã¡ã§ãæ¬æ¥ViewModelã§å®çµãã¹ãããžãã¯ãViewåŽã«æŒãåºããŠããŸãããViewModelã®ããããã£ãçŽæ¥èªã¿åã£ãŠæ¡ä»¶åå²ããå®è£
ããè€æ°ã¡ãœãããç¹å®ã®çµã¿åããã§åŒã³åºãå®è£
ãåæã«ååšããŠããŸããã // ViewãViewModelã®ããããã£ãçŽæ¥èªã¿åã£ãŠToast衚瀺ãå¶åŸ¡ããŠãã val context = LocalContext.current Button( onClick = { viewModel.increment() if (viewModel.currentCount == 10 ) { Toast.makeText(context, "10ã«å°éããŸãã" , Toast.LENGTH_SHORT).show() } } ) { Text( "Increment" ) } // 1ã€ã®ãŠãŒã¶ãŒæäœã«å¯ŸããŠViewåŽãè€æ°ã¡ãœãããçµã¿åãããŠåŒãã§ãã Button( onClick = { viewModel.increment() viewModel.checkLimit() } ) { Text( "Increment" ) } ãã®ããã«ViewãViewModelã®æ§é ãç¥ããããŠãããããæ©èœå€æŽæã®åœ±é¿ç¯å²ãåºããããããªããã¬ãã¥ãŒè² è·ãäžå
·åã®åå ã«ãªã£ãŠããŸããã ã€ãã³ãéç¥ãšç»é¢é·ç§»ã®äžçµ±äž Toast衚瀺ãç»é¢é·ç§»ãšãã£ãäžåºŠããã®åŠçã«ã€ããŠãå®è£
ãã¿ãŒã³ãæç¢ºã«çµ±äžãããŠããŸããã§ãããToast衚瀺ã§ã¯ViewModelããã€ãã³ããçºè¡ããŠViewåŽã§è³Œèªãããã¿ãŒã³ãšãViewåŽã§StateãçŽæ¥ç£èŠããŠåŠçãããã¿ãŒã³ãæ··åšããŠããŸããã // CounterScreen.kt: ViewModelã®ã€ãã³ãçµç±ã§Toast衚瀺 LaunchedEffect( Unit ) { viewModel.event.collect { event -> when (event) { is CounterEvent.ShowToast -> Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() } } } Button(onClick = { viewModel.increment() }) { Text( "Increment" ) } // CounterScreen.kt: ViewåŽã§StateãçŽæ¥ç£èŠããŠToast衚瀺 val counter by viewModel.counter.collectAsStateWithLifecycle() LaunchedEffect(counter) { if (counter >= 10 ) { Toast.makeText(context, "10ã«å°éããŸãã" , Toast.LENGTH_SHORT).show() } } Button(onClick = { viewModel.increment() }) { Text( "Increment" ) } ç»é¢é·ç§»ã«ã€ããŠãViewModelã®ã€ãã³ãçµç±ã§é·ç§»ãããã¿ãŒã³ãšãComposable颿°ããçŽæ¥NavigatorãåŒã³åºããã¿ãŒã³ãæ··åšããŠããŸããã // CounterScreen.kt: ViewModelã®ã€ãã³ãçµç±ã§ç»é¢é·ç§» LaunchedEffect( Unit ) { viewModel.event.collect { event -> when (event) { is CounterEvent.NavigateSetting -> navController.navigateSetting() } } } Button(onClick = { viewModel.navigateSetting() }) { Text( "Setting" ) } // CounterScreen.kt: Composable颿°ããçŽæ¥NavigatorãåŒã³åºããŠç»é¢é·ç§» Button(onClick = { navController.navigateSetting() }) { Text( "Setting" ) } æ¹åŒãçµ±äžãããŠããªããããæ°ããç»é¢ãå®è£
ããéã«ã©ã®æ¹åŒãžåãããã¹ãã倿ãã¥ãããéçºè
ããšã®å®è£
ã®ã°ãã€ããæããŠããŸãããããã«StateãçŽæ¥ç£èŠããæ¹åŒã§ã¯ãç»é¢ã«æ»ã£ãŠããéã«ã€ãã³ããåçºç«ããŠæå³ããªãåäœãçºçããäžå
·åãèµ·ããŠããŸããã ç§ãã¡ã®MVVMã¢ãŒããã¯ãã£ã®æ¹åæ¹é ãããã®åé¡ãæŸçœ®ããã°éçºå¹çã»å質ãšãã«äœäžãç¶ãããããå課é¡ã«å¯ŸããŠä»¥äžã®ãããªè§£æ±ºæ¹éãèãããŸãã¯æ¢åã®MVVMã¢ãŒããã¯ãã£ã®æ çµã¿ã®äžã§æ¹åã§ããªããæ€èšãé²ããŸããã èª²é¡ è§£æ±ºæ¹é State管çã®è€éå ç»é¢ã®ç¶æ
ã1ã€ã®data classã«éçŽããåäžã®StateFlowã§ç®¡çãã ViewãšViewModelã®è²¬åãææ§ ãŠãŒã¶ãŒæäœãã€ãã³ããšããŠå®çŸ©ããåŠçãViewModelå
ã«éçŽãã ã€ãã³ãéç¥ãšç»é¢é·ç§»ã®äžçµ±äž ã€ãã³ãéç¥ãChannelã«çµ±äžããç»é¢é·ç§»ãã€ãã³ãçµç±ã«çµ±äžãã UiStateã«ããState管çã®åçŽå SSOTã®ååã«åŸããç»é¢ã®ç¶æ
ã1ã€ã®data classã«éçŽããŠåäžã®StateFlowã§ç®¡çããæ¹éãèããŸãããViewã¯ä¿¡é Œã§ããå¯äžã®ãœãŒã¹ã賌èªããŠç»é¢ã«åæ ããã ãã®ã·ã³ãã«ãªæ§é ã«ãªããŸãããŸãç¶æ
ã®æŽæ°ã _state.update ã«éçŽãããããã Flow.map ã combine ã«ããåæãäžèŠã«ãªããæŽæ°ã¿ã€ãã³ã°ãå¶åŸ¡ãããããªããšèããŸããã // CounterUiState.kt: ç»é¢ã®ç¶æ
ã1ã€ã®data classã«éçŽããæŽŸçå€ãdata classå
ã§èšç®ãã data class CounterUiState( val count: Int = 0 , ) { val doubleCount: Int get () = count * 2 val tripleCount: Int get () = count * 3 } // CounterViewModel.kt: åäžã®StateFlowã§ç®¡çãããŠãŒã¶ãŒæäœããšã«ã¡ãœãããå®çŸ© class CounterViewModel : ViewModel() { private val _state = MutableStateFlow(CounterUiState()) val state: StateFlow<CounterUiState> = _state.asStateFlow() fun onIncrementClicked() { _state.update { it.copy(count = it.count + 1 ) } } } // CounterScreen.kt: ViewåŽã¯åäžã®Stateã賌èªããã ã @Composable fun CounterScreen(viewModel: CounterViewModel, /* ... */ ) { val state by viewModel.state.collectAsStateWithLifecycle() CounterScreenContent( state = state, onIncrement = viewModel :: onIncrementClicked, // ... ) } ãŠãŒã¶ãŒæäœããšã®ã¡ãœããå®çŸ©ã«ãã責åã®æç¢ºå UDFã®ååã«åŸããViewããã®ActionïŒãŠãŒã¶ãŒæäœïŒã«åå¿ããŠStateãæŽæ°ãããã·ã³ãã«ãªæ§é ãèããŸããããŠãŒã¶ãŒæäœããšã«ã¡ãœãããå®çŸ©ããé¢é£ããæŽæ°åŠçããã¹ãŠãã®ã¡ãœããå
ã«éçŽããŸããããã«ããViewåŽã¯ãŠãŒã¶ãŒæäœãViewModelã«äŒããã ãã®åœ¹å²ã«ãªããå
·äœçãªåŠçã¯ãã¹ãŠViewModelåŽã§å®çµããããã責åãæç¢ºã«ãªããšèããŸããã // CounterViewModel.kt: ãŠãŒã¶ãŒæäœïŒActionïŒããšã«ã¡ãœãããå®çŸ©ããåŠçãViewModelå
ã«éçŽ class CounterViewModel : ViewModel() { private val _state = MutableStateFlow(CounterUiState()) val state: StateFlow<CounterUiState> = _state.asStateFlow() fun onIncrementClicked() { viewModelScope.launch { _state.update { it.copy(count = it.count + 1 ) } checkLimit() } } private suspend fun checkLimit() { /* ... */ } } // CounterScreen.kt: Viewã¯Actionãçºè¡ããã ã CounterScreenContent( state = state, onIncrement = viewModel :: onIncrementClicked, onDecrement = viewModel :: onDecrementClicked, onReset = viewModel :: onResetClicked, ) Channelã«ããã€ãã³ãéç¥ãšç»é¢é·ç§»ã®çµ±äž ã€ãã³ãéç¥ãšç»é¢é·ç§»ã®æ¹åŒãChannelã«çµ±äžããæ¹éãèããŸãããäžåºŠéãã®ã€ãã³ããsealed classã§å®çŸ©ããChannelã§é
ä¿¡ããããšã§ãStateFlowã®ããã«ç¶æ
ãšããŠä¿æãããååä¿¡ã«ããäžå
·åãé²ããŸãã ç»é¢é·ç§»ãã€ãã³ãã®äžçš®ãšããŠæ±ãããã¹ãŠViewModelçµç±ã§çºè¡ãã圢ã«çµ±äžããŸããåçŽãªé·ç§»ã§ããã°ViewããçŽæ¥åŒã³åºãæ¹ãã·ã³ãã«ã§ãããå®éã«ã¯é·ç§»åã®æ¡ä»¶ãã§ãã¯ããã©ã¡ãŒã¿ã®çµã¿ç«ãŠãå¿
èŠã«ãªãã±ãŒã¹ãå€ãã§ãããã®ããViewModelåŽã«éçŽããæ¹ãäžè²«æ§ãä¿ã¡ããããšå€æããŸããã // CounterEvent.kt: ã€ãã³ããšç»é¢é·ç§»ãsealed classã§å®çŸ© sealed class CounterEvent { data class ShowToast( val message: String ) : CounterEvent() data object NavigateSetting : CounterEvent() } // CounterViewModel.kt: ã€ãã³ãéç¥ãšç»é¢é·ç§»ãChannelã§çµ±äžçã«é
ä¿¡ class CounterViewModel : ViewModel() { private val _state = MutableStateFlow(CounterUiState()) val state: StateFlow<CounterUiState> = _state.asStateFlow() private val _event = Channel<CounterEvent>(Channel.BUFFERED) val event: Flow<CounterEvent> = _event.receiveAsFlow() fun onIncrementClicked() { viewModelScope.launch { _state.update { it.copy(count = it.count + 1 ) } checkLimit() } } fun onSettingClicked() { viewModelScope.launch { _event.send(CounterEvent.NavigateSetting) } } private suspend fun checkLimit() { val count = _state.value.count if (count >= 10 ) { _event.send(CounterEvent.ShowToast( "10ã«å°éããŸãã" )) } } } // CounterScreen.kt: ã€ãã³ããChannelã§çµ±äžçã«è³Œèªããç»é¢é·ç§»ãToastãäžå
çã«åŠç LaunchedEffect( Unit ) { viewModel.event.collect { event -> when (event) { is CounterEvent.ShowToast -> Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() CounterEvent.NavigateSetting -> onNavigateSetting() } } } ç§ãã¡ã®MVVMã¢ãŒããã¯ãã£ã®æ¹åæ¹éãéçšã§ããã ãããŸã§ç޹ä»ããæ¹åæ¹éã¯ãSSOTã«åºã¥ãStateéçŽãUDFã«åºã¥ãActionå®çŸ©ãChannelã«ããã€ãã³ãéç¥ã®çµ±äžã§ãããããã¯æ¢åã®MVVMã¢ãŒããã¯ãã£ã®æ çµã¿ã§å®çŸã§ããããšãããããŸããããããã«ãŒã«ãšããŠå®ããã ãã§ã¯ãè€æ°äººéçºã®äžã§åŸã
ã«åœ¢éªžåããŠããããšã課é¡ãšããŠãããŸããã UiStateã«ãŸãšããã«ãŒã«ããã£ãŠããæ¥ãã®å¯Ÿå¿ã§æ°ããStateFlowã远å ãããå
ã®èšèšã«æ»ã£ãŠããŸã ãŠãŒã¶ãŒæäœããšã«ã¡ãœãããå®çŸ©ããæ¹éã§ããViewåŽããè€æ°ã¡ãœãããçŽæ¥åŒã³åºãå®è£
ãã¬ãã¥ãŒãããæããŠããŸã Channelã«çµ±äžããã«ãŒã«ããã£ãŠããæ¢åã³ãŒããåèã«StateFlowã§ã€ãã³ãéç¥ãå®è£
ããŠããŸã ãŸãæ¹åæ¹éãåç»é¢ã§æçŽã«å®è£
ãããšãStateFlowãChannelã®å®çŸ©ã»è³Œèªãšãã£ããã€ã©ãŒãã¬ãŒããç»é¢ããšã«å¢å ããããšã課é¡ã§ããã MVIã¢ãŒããã¯ãã£ã®å°å
¥ãšç¬èªã©ã€ãã©ãªã®äœæ ãããã®èª²é¡ãããã«ãŒã«ã§ã¯ãªãä»çµã¿ãšããŠæ£ããå®è£
ã«å°ããããããMVIã¢ãŒããã¯ãã£ãå°å
¥ããããšã«ããŸããã MVIã¢ãŒããã¯ãã£ã®å°å
¥ã«ããããæ¢åã®OSSã©ã€ãã©ãªãæ€èšããŸããããããç§ãã¡ãå¿
èŠãšããŠããã®ã¯ã·ã³ãã«ãªMVIã®ããŒã¿ãããŒã§ãããæ¢åã®OSSã©ã€ãã©ãªã¯å€æ©èœã§åŠç¿ã³ã¹ããé«ããšæããŸãããå®çŸã«å¿
èŠãªã³ãŒãéãå°ãªãèªåãã¡ã§éçºã§ããèŠæš¡ã ã£ãããããããžã§ã¯ãã®ç¹æ§ã«åãããç¬èªMVIã©ã€ãã©ãªãäœæããããšã«ããŸããã ããŒã¿ãã㌠ç¬èªMVIã©ã€ãã©ãªã§ã¯ãåè¿°ã®æ¹åæ¹éãMVIã®èšèšææ³ã«æ²¿ã£ãŠæŽçããããšã«ããŸãããMVIã®Stateã»Viewã»Actionã«å ããŠãç»é¢é·ç§»ãToast衚瀺ãšãã£ãäžåºŠéãã®ã€ãã³ããæ±ãSideEffectãå°å
¥ããŠããŸãã èŠçŽ åœ¹å² å¯Ÿå¿ããæ¹åæ¹é State ç»é¢ã®çŸåšç¶æ
ã衚ãåäžã®data classãUIã¯ãã®å€ã®ã¿ããæ§ç¯ãããã SSOTã«åºã¥ãStateéçŽ View Stateãåãåã£ãŠç»é¢ã«åæ ãããŠãŒã¶ãŒæäœãActionãšããŠçºè¡ããã - Action ãŠãŒã¶ãŒæäœãViewããViewModelãžäŒããå
¥åã UDFã«åºã¥ãActionå®çŸ© SideEffect ç»é¢é·ç§»ãToast衚瀺ãªã©ãäžåºŠéãã®ã€ãã³ããChannelã§Viewã«é
ä¿¡ãããã Channelã«ããã€ãã³ãéç¥çµ±äž ViewããActionãéä¿¡ããããšãViewModelããããåãåã£ãŠStateãæŽæ°ããããSideEffectãçºè¡ããŸãããã®ã·ã³ãã«ãªããŒã¿ãããŒã«ããããŠãŒã¶ãŒæäœãã©ã®ããã«åŠçãããããäžè²«ããæµãã§è¿œããããã«ããŠããŸãã å®è£
ã€ã³ã¿ãã§ãŒã¹ã®å®çŸ© ãŸããMVIã®åèŠçŽ ã«å¯Ÿå¿ããããŒã«ãŒã€ã³ã¿ãã§ãŒã¹ãšã㊠MVIState ã» MVIAction ã» MVISideEffect ãå®çŸ©ããŸãããåç»é¢ã®Stateã»Actionã»SideEffectã¯ã©ã¹ãžããããå®è£
ãããããšã§ãåãã©ã¡ãŒã¿ã®å¶çŽãšããŠå©çšãã誀ã£ãåã®çµã¿åãããã³ã³ãã€ã«æã«æ€åºã§ããŸãã æ¬¡ã«ãMVIã®ããŒã¿ãããŒãå®çŸããããã® MVI ã€ã³ã¿ãã§ãŒã¹ãå®çŸ©ããŸãããStateã®è³ŒèªïŒ state ïŒãActionã®åãåãïŒ onAction ïŒãStateã®æŽæ°ïŒ update ïŒãSideEffectã®çºè¡ïŒ sideEffect ïŒãéçŽããŠããŸãã interface MVIState interface MVIAction interface MVISideEffect interface MVI<State : MVIState, Action : MVIAction, SideEffect : MVISideEffect> { val state: StateFlow<State> val currentState: State val sideEffect: Flow<SideEffect> fun onAction(action: Action) suspend fun update(block: suspend (State) -> State) suspend fun sideEffect(effect: SideEffect) } ç§»è²ãçšããã€ã³ã¿ãã§ãŒã¹ã®å®è£
次ã«ããã®ã€ã³ã¿ãã§ãŒã¹ã®å®è£
ã¯ã©ã¹ãšã㊠MVIDelegate ãçšæããŸãããå
éšã§ã¯StateãMutableStateFlowã§ç®¡çããSideEffectãChannelã§é
ä¿¡ããŠããŸããViewModelã§ã¯Kotlinã®ããªã²ãŒããã¿ãŒã³ïŒ by mvi(...) ïŒã䜿ãããšã§ã MVI ã€ã³ã¿ãã§ãŒã¹ã®æ©èœãViewModelãžè¿œå ã§ããããã«ããŸããã class MVIDelegate<State : MVIState, Action : MVIAction, SideEffect : MVISideEffect>( initialState: State, ) : MVI<State, Action, SideEffect> { private val _state = MutableStateFlow(initialState) override val state: StateFlow<State> = _state.asStateFlow() override val currentState: State get () = _state.value private val _sideEffect by lazy { Channel<SideEffect>() } override val sideEffect: Flow<SideEffect> by lazy { _sideEffect.receiveAsFlow() } override fun onAction(action: Action) {} override suspend fun sideEffect(effect: SideEffect) { ... } override suspend fun update(block: suspend (State) -> State) { ... } } fun <State : MVIState, Action : MVIAction, SideEffect : MVISideEffect> mvi( initialUiState: State, ): MVI<State, Action, SideEffect> = MVIDelegate( initialState = initialUiState, savedStateHandle = null , savedStateName = null , ) ãŸããJetpack ComposeãšMVIãæ¥ç¶ããããã® MviContent ã³ã³ããŒã¶ãã«ãæäŸããŠããŸããå
éšã§StateãšSideEffectã賌èªããContentå±€ã«ã¯ state ãš onAction ã®ã¿ãæž¡ãããŸããéçºè
ã¯è³Œèªã®ä»æ¹ãæèããçŽç²ãªComposable颿°ãæžãã ãã§æžãããã«ããŸããã @Composable fun <State : MVIState, Action : MVIAction, SideEffect : MVISideEffect> MviContent( viewModel: MVI<State, Action, SideEffect>, sideEffect: suspend (SideEffect) -> Unit , content: @Composable (state: State, onMviAction: (Action) -> Unit ) -> Unit , ) { LaunchedEffect( Unit ) { viewModel.sideEffect.collect { sideEffect(it) } } val state by viewModel.state.collectAsStateWithLifecycle() content(state, viewModel :: onAction) } MVIã¢ãŒããã¯ãã£ãç¬èªMVIã©ã€ãã©ãªã§å®è£
ãã ããããã¯ãç¬èªMVIã©ã€ãã©ãªã䜿ã£ãŠå®éã«Counterç»é¢ãMVIã¢ãŒããã¯ãã£ã§å®è£
ããäŸã玹ä»ããŸããContractã»ViewModelã»Screenã»ãã¹ãã®é ã«ãæ¹åæ¹éãã©ã®ããã«ã³ãŒãã«åæ ããããã確èªããŠãããŸãã Contract: Stateã»Actionã»SideEffectã®å®çŸ© ç»é¢ã«å¿
èŠãªStateã»Actionã»SideEffectãã1ã€ã®Contractãã¡ã€ã«ã«ãŸãšããŠå®çŸ©ããŸããSSOTã®ååã«åŸãç»é¢ã®ç¶æ
ã CounterState ã«éçŽããUDFã®ååã«åŸããŠãŒã¶ãŒæäœã CounterAction ãšããŠåæããŠããŸããäžåºŠéãã®ã€ãã³ã㯠CounterSideEffect ãšããŠå®çŸ©ããŸããç»é¢ãæ±ãããŒã¿ã®å
šäœåããã®ãã¡ã€ã«ã ãã§ææ¡ã§ããŸãã // CounterContract.kt // SSOT: ç»é¢ã®ç¶æ
ã1ã€ã®data classã«éçŽ data class CounterState( val count: Int = 0 , ) : MVIState { val doubleCount: Int get () = count * 2 val tripleCount: Int get () = count * 3 companion object { val initialState = CounterState() } } // UDF: ãŠãŒã¶ãŒæäœãActionãšããŠåã§å®çŸ© sealed class CounterAction : MVIAction { data object Increment : CounterAction() data object Decrement : CounterAction() data object Reset : CounterAction() data object ClickSetting : CounterAction() } // Channel: äžåºŠéãã®ã€ãã³ããšç»é¢é·ç§»ãSideEffectãšããŠå®çŸ© sealed class CounterSideEffect : MVISideEffect { data class ShowToast( val message: String ) : CounterSideEffect() data object NavigateSetting : CounterSideEffect() } ViewModel: Actionã®åŠçãšStateæŽæ° ViewModelã§ã¯ MVI ã€ã³ã¿ãã§ãŒã¹ãããªã²ãŒããã¿ãŒã³ïŒ by mvi(...) ïŒã§å©çšããŸãã by mvi() ã䜿ãããšã§StateFlowãçšããState管çãšChannelãéããSideEffecté
ä¿¡ãã©ã€ãã©ãªåŽã§åŒ·å¶ããããããéçºè
ãç¬èªã«Flowãå®çŸ©ããäœå°ããªããªããŸãããã¹ãŠã®ãŠãŒã¶ãŒæäœã¯ onAction ã§äžå
çã«åãåããŸããActionã®çš®é¡ã«å¿ã㊠update ã§StateãæŽæ°ãã sideEffect ãéããŠã€ãã³ããéä¿¡ããŸãã // CounterViewModel.kt @HiltViewModel class CounterViewModel @Inject constructor () : ViewModel(), MVI<CounterState, CounterAction, CounterSideEffect> by mvi(CounterState.initialState) { override fun onAction(action: CounterAction) { viewModelScope.launch { when (action) { CounterAction.Increment -> reduceIncrement() CounterAction.Decrement -> reduceDecrement() CounterAction.Reset -> reduceReset() CounterAction.ClickSetting -> sideEffect(CounterSideEffect.NavigateSetting) } } } private suspend fun reduceIncrement() { update { it.copy(count = it.count + 1 ) } checkLimit() } private suspend fun reduceDecrement() { update { it.copy(count = it.count - 1 ) } } private suspend fun reduceReset() { update { CounterState.initialState } } private suspend fun checkLimit() { val count = currentState.count if (count == 10 ) { sideEffect(CounterSideEffect.ShowToast( "10ã«å°éããŸãã" )) } } } ViewããActionãéä¿¡ããã onAction å
ã§ãã®Actionã«å¯ŸããåŠçããã¹ãŠå®çµããŸããViewåŽãè€æ°ã¡ãœãããçµã¿åãããŠåŒã³åºãå¿
èŠããªããªããåŒã³å¿ããé åºãããæ§é çã«çºçããªããªããŸããç»é¢é·ç§»ãSideEffectãšã㊠onAction å
ããçºè¡ããããããé·ç§»ã®èµ·ç¹ãViewModelåŽã«éçŽãããŸãã View: MviContentã«ããCompose飿º ãã®äŸã§ã¯ãViewå±€ãScreenãšContentã«åããŠå®è£
ããŠããŸããScreenã§ã¯ MviContent ã䜿ã£ãŠStateã®è³ŒèªãšSideEffectã®åŠçãæ¥ç¶ããŸãã MviContent ã®å
éšã§StateãšSideEffectã®è³Œèªãè¡ããããããContentã«ã¯ state ãš onAction ã®ã¿ãæž¡ãããŸããContentã¯Stateã衚瀺ããŠActionãéä¿¡ããã ãã®çŽç²ãªComposable颿°ã«ãªããŸãã // CounterScreen.kt @Composable fun CounterScreen( mvi: MVI<CounterState, CounterAction, CounterSideEffect>, onNavigateSetting: () -> Unit , modifier: Modifier = Modifier, ) { val context = LocalContext.current MviContent( viewModel = mvi, sideEffect = { effect -> when (effect) { is CounterSideEffect.ShowToast -> Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() CounterSideEffect.NavigateSetting -> onNavigateSetting() } }, ) { state, onAction -> CounterScreenContent( state = state, onIncrement = { onAction(CounterAction.Increment) }, onDecrement = { onAction(CounterAction.Decrement) }, onReset = { onAction(CounterAction.Reset) }, onSettingClick = { onAction(CounterAction.ClickSetting) }, modifier = modifier, ) } } @Composable private fun CounterScreenContent( state: CounterState, onIncrement: () -> Unit , onDecrement: () -> Unit , onReset: () -> Unit , onSettingClick: () -> Unit , modifier: Modifier = Modifier, ) { Column(modifier = modifier) { Text(text = "Count: ${ state.count } " , fontSize = 32 .sp) Button(onClick = onIncrement) { Text(text = "+" ) } Button(onClick = onDecrement) { Text(text = "-" ) } Button(onClick = onReset) { Text(text = "Reset" ) } Button(onClick = onSettingClick) { Text(text = "Setting" ) } } } Flowããšã« collectAsState ã䞊ã¹ãå¿
èŠããªããªããViewåŽãnavControllerãViewModelã®å
éšç¶æ
ã«äŸåããæ§é ãè§£æ¶ãããŸããç»é¢é·ç§»ãToast衚瀺ã¯ãã¹ãŠSideEffectçµç±ã®ã³ãŒã«ããã¯ã«çµ±äžããããããContentã®è²¬åãã·ã³ãã«ã«ä¿ãããŸããViewModelã«äŸåããªãComposable颿°ãçšæããããšã§ãPreview颿°ãå®çŸ©ãããããªããŸãã ãã¹ã: Actionãéä¿¡ããŠStateã»SideEffectãæ€èšŒ MVIã¢ãŒããã¯ãã£ã§ã¯ããŒã¿ãããŒãäžæ¹åã«åºå®ãããŠããããããã¹ãããActionãéä¿¡ããŠãStateã®å€åãŸãã¯SideEffectã®çºè¡ãæ€èšŒããããšãããã¿ãŒã³ã«çµ±äžãããŸãããã¹ã察象ã®å
¥åãšåºåãæç¢ºãªã®ã§ãäœããã¹ããã¹ãããèªç¶ãšå®ãŸããŸãã // CounterViewModelTest.kt class CounterViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() private lateinit var target: CounterViewModel @BeforeTest fun setup() { target = CounterViewModel() } // Stateã®å€åãæ€èšŒ @Test fun `Action - Increment - increases count by 1`() = runTest { target.state.test { assertEquals( 0 , awaitItem().count) target.onAction(CounterAction.Increment) val state = awaitItem() assertEquals( 1 , state.count) assertEquals( 2 , state.doubleCount) assertEquals( 3 , state.tripleCount) } } // SideEffectã®çºè¡ãæ€èšŒ @Test fun `Action - ClickSetting - emits NavigateSetting side effect`() = runTest { target.sideEffect.test { target.onAction(CounterAction.ClickSetting) assertEquals(CounterSideEffect.NavigateSetting, awaitItem()) } } // StateæŽæ°ãšSideEffectã®çµã¿åãããæ€èšŒ @Test fun `Action - Increment - emits ShowToast when count reaches 10`() = runTest { repeat( 9 ) { target.onAction(CounterAction.Increment) } target.sideEffect.test { target.onAction(CounterAction.Increment) assertEquals(CounterSideEffect.ShowToast( "10ã«å°éããŸãã" ), awaitItem()) } } } MVVMã¢ãŒããã¯ãã£ããMVIã¢ãŒããã¯ãã£ã«ç§»è¡ããŠã¿ãŠ ãã®ãããªç¬èªMVIã©ã€ãã©ãªã䜿ã£ãMVIã¢ãŒããã¯ãã£ãžã®ç§»è¡ã¯2024幎9æã«éå§ããŸãããæ¢åç»é¢ãäžæ¬ã§ç§»è¡ããã®ã§ã¯ãªãããæ°èŠç»é¢ã¯ååMVIããæ¢åç»é¢ã¯æ¹ä¿®ã¿ã€ãã³ã°ã§çœ®ãæãããšããã«ãŒã«ã«ããç»é¢åäœã§æ®µéçã«é²ããŠããŸããããã«ããéçºãæ¢ããããšãªãç§»è¡ãé²ããããç»é¢ããšã®ãªã¹ã¯ãå°ããä¿ã£ããŸãŸé©çšç¯å²ãåºããããšãã§ããŠããã2026幎2æçŸåšã段éçãªç§»è¡ãç¶ç¶ããŠããŸãã 2024幎9æ 2025幎4æ 2025幎10æ çŸåš MVI 1ïŒ2.2ïŒ
ïŒ 11ïŒ24.4ïŒ
ïŒ 21ïŒ38.9ïŒ
ïŒ 31ïŒ50.8ïŒ
ïŒ MVVM 44ïŒ97.8ïŒ
ïŒ 34ïŒ75.6ïŒ
ïŒ 33ïŒ61.1ïŒ
ïŒ 30ïŒ49.2ïŒ
ïŒ åèš 45 45 54 61 ãã®ããã«MVIã®å®è£
ãåŸã
ã«å¢ããäžã§ãåè¿°ã®ã¢ãŒããã¯ãã£äžã®èª²é¡ãè§£æ¶ãããããšã«å ããéçºå·¥çšãã®ãã®ã«ã以äžã®ãããªã¡ãªãããåºãŠããŠããŸãã ããŒã å
šäœã§äžè²«ããå®è£
ãã§ããããã«ãªã£ã ç¬èªMVIã©ã€ãã©ãªãäœãå®è£
æ¹éãæ±ºããããããŠããã¥ã¡ã³ããæŽåã»å
¬éããããšã§ãã©ã€ãã©ãªãšããã¥ã¡ã³ãã®äž¡é¢ããããŒã å
šäœã§äžè²«ããå®è£
ãé²ããããããã«ãªããŸããã æ°ããã¡ã³ããŒãå ãã£ãéãã1ã€ã®ç»é¢ã®Contractã»ViewModelã»Viewãèªãã°ãããžã§ã¯ãå
šäœã®å®è£
ãã¿ãŒã³ãçè§£ã§ããŸãããªã³ããŒãã£ã³ã°ã®è² è·ã軜æžãããŠãããšæããŠããŸãã PRã¬ãã¥ãŒã®è³ªãåäžãã ããŒã å
šäœã§å®è£
æ¹éãçµ±äžã§ããããã«ãªããåºæ¬çãªããŒã¿ãããŒã«é¢ããææã¯å€§ããæžããŸããã以åã¯ãå®è£
ãã¿ãŒã³ã®çµ±äžã«é¢ããã³ã¡ã³ããã¬ãã¥ãŒã®å€ããå ããŠããŸãããMVIã©ã€ãã©ãªã«ãã£ãŠããããæ§é çã«è§£æ¶ãããããšã§ãã¬ãã¥ãŒã®çŠç¹ãå€ãããŸãããçŸåšã¯ã仿§ã®åŠ¥åœæ§ã®ç¢ºèªãã³ãŒãã®ãã©ãã·ã¥ã¢ããã«ãããå€ãã®æéã䜿ããããã«ãªããŸããã AIã³ãŒãã£ã³ã°ãšãŒãžã§ã³ããšã®åæ¥ããããããªã£ã çŸåšãAIã³ãŒãã£ã³ã°ãšãŒãžã§ã³ãã®DevinãæŽ»çšããæ¢åç»é¢ã®MVIç§»è¡ã«ããã£ã¬ã³ãžããŠããŸããMVIã¢ãŒããã¯ãã£ã§ã¯Stateã»Actionã»SideEffectãšããæç¢ºãªæ§é ããããããDevinãçæããã³ãŒãã§ãåŠçã®æµãã远ãããããã¬ãã¥ãŒããããã§ããã¢ãŒããã¯ãã£ãçµ±äžãããŠããããšã¯ã人éå士ã®éçºã ãã§ãªããAIãšã®åæ¥ã«ãããŠã倧ããªã¡ãªããã«ãªããšæããŠããŸãã ãŸãšã æ¬èšäºã§ã¯ãZOZOFITã®Androidã¢ããªã«ãããMVVMã¢ãŒããã¯ãã£ã®èª²é¡ãšãMVIã¢ãŒããã¯ãã£ãžã®ç§»è¡ãç¬èªMVIã©ã€ãã©ãªã®éçºã«ã€ããŠç޹ä»ããŸãããMVIã¢ãŒããã¯ãã£ã¯ããŠãŒã¶ãŒäœéšã®äœäžãæªç¶ã«é²ãä»çµã¿ãšããŠãæ©èœããŠãããšæããŠããŸããZOZOFITã®å©çšè
ãæ¥ã
å¢ãããªãã§ãäœéšãå®å®ããŠæ¯ããããããããããããã¢ãŒããã¯ãã£ã®æ¹åãé²ããŠãããŸããæåŸãŸã§ãèªã¿ããã ããããããšãããããŸããã ZOZOã§ã¯ãäžç·ã«ãµãŒãã¹ãäœãäžããŠãããæ¹ãåéäžã§ãããèå³ã®ããæ¹ã¯ã以äžã®ãªã³ã¯ãããã²ãå¿åãã ããã corp.zozo.com