| name | changing-feature-behavior |
| description | Use when adding or changing behavior in an existing feature screen, including MVI State, Intent, Effect, ViewModel reducers, one-shot effects, API result handling, or UI event wiring in this Android repository. |
Changing Feature Behavior
๊ธฐ์กด ํ๋ฉด ๋์ ๋ณ๊ฒฝ์ Contract, ViewModel, Route/Screen ์ฐ๊ฒฐ์ ํจ๊ป ๋ง์ถ๋ค.
์์
๋ฒ์
feature/<feature-name>/impl/src/main/.../*Contract.kt
feature/<feature-name>/impl/src/main/.../*ViewModel.kt
feature/<feature-name>/impl/src/main/.../*Screen.kt
- ํ๋ฉด ์ ์ฉ callback์ ๋ฐ๋ feature ๋ด๋ถ
component/*
navigation/*EntryProvider.kt (๊ฒฐ๊ณผ ์์ ๋ก์ง์ ์์ ํ๋ ๊ฒฝ์ฐ)
- ์ผ๋ถ ํ๋ฉด(mypage์ profile ๊ณ์ด ๋ฑ)์ ๋ณ๋ Contract ์์ด ์์ ํ๋ฉด์ ViewModel/Contract๋ฅผ ๊ณต์ ํ๋ฏ๋ก, effect ์ถ๊ฐ ์ ์ด๋ฅผ ์์ ํ๋ ๋ชจ๋ Route๋ฅผ ํจ๊ป ํ์ธํ๋ค.
๊ตฌํ ํ๋ฆ
- Contract์์ ๋ณ๊ฒฝ ๋์์ด
State, Intent, Effect ์ค ์ด๋์ ์ํ๋์ง ์ ํ๋ค.
- Screen ๋๋ component callback์์
onIntent(...)๋ฅผ ํธ์ถํ๋ค.
- ViewModel์
onIntent ๋ถ๊ธฐ์ ์ intent๋ฅผ ์ถ๊ฐํ๋ค.
- ํ๋ฉด ์ํ ๋ณ๊ฒฝ์
reduce { copy(...) }๋ก ์ฒ๋ฆฌํ๋ค.
- navigation, toast, launcher, platform action์
postSideEffect(...)๋ก ๋ณด๋ธ๋ค.
- repository/usecase ๊ฒฐ๊ณผ๋
onSuccess์ onFailure๋ก ๋๋๋ค.
- Route์ effect collect์์ ์ธ๋ถ ๋์์ ์คํํ๋ค.
- ์ repository/service/DataStore ๊ตฌํ์ด ๋ถ์ผ๋ฉด
implementing-remote-api ๋๋ implementing-local-preference๋ฅผ ํจ๊ป ์ ์ฉํ๋ค.
Contract
State: Screen์ด ๊ทธ๋ฆด ํ๋ฉด ์ํ์ด๋ค.
Intent: Screen ๋๋ component์์ ViewModel๋ก ๋ณด๋ด๋ ์
๋ ฅ์ด๋ค.
Effect/SideEffect: ViewModel์์ Route๋ก ๋ณด๋ด๋ one-shot output์ด๋ค. ์ ๋ฏธ์ฌ๋ ํ๋ฉด๋ง๋ค ํผ์ฌํ๋ฏ๋ก ๊ธฐ์กด ํ๋ฉด ์์ ์ ํด๋น Contract์ ๊ธฐ์กด ์ ๋ฏธ์ฌ๋ฅผ ๋ฐ๋ฅธ๋ค.
ViewModel ์ฒ๋ฆฌ ๊ธฐ์ค
- ์ด๊ธฐ ๋ก๋ฉ์
Enter<Screen>Screen intent๋ฅผ init { store.onIntent(...) }์์ ๋ฐํํด ์ฒ๋ฆฌํ๋ค(๋ค์). ์ผ๋ถ ํ๋ฉด์ mviIntentStore(initialFetchData = ...)๋ก uiState ์ฒซ ๊ตฌ๋
์ ๋ฐํํ๋ฉฐ, ํ๋ฉด ์ง์
๋์ ๋ณ๊ฒฝ์ ์ด intent ๋ถ๊ธฐ์์ ์์ ํ๋ค.
onIntent๋ intent ๋ถ๊ธฐ์ ์งง์ state/effect ์ฒ๋ฆฌ์ ๋๋ค.
- ์กฐ๊ฑด์ด ์๊ฑฐ๋ ๋น๋๊ธฐ ์์
์ด ์์ผ๋ฉด private ํจ์๋ก ๋ถ๋ฆฌํ๋ค.
- private ํจ์๋ ํ์ํ ๊ฐ๋ง
state, reduce, postSideEffect๋ก ๋ฐ๋๋ค.
- ํจ์ ์ญํ ์
handle..., fetch..., load..., perform..., save..., update..., delete..., preload... ๊ณ์ด๋ก ๋๋ฌ๋ธ๋ค.
- ์ฐ์ ์
๋ ฅ์ ํฉ์ณ ์ฒ๋ฆฌํ๋ ๋์์
MutableSharedFlow์ debounce๋ฅผ ์ด๋ค.
Effect ์ด๋ฆ
๊ฐ์ ์ฑ๊ฒฉ์ effect๊ฐ ์ด๋ฏธ ์์ผ๋ฉด ํด๋น Contract์ ๊ธฐ์กด ์ด๋ฆ์ ๋ฐ๋ฅธ๋ค.
- ๋ค๋ก ์ด๋:
NavigateBack
- ํน์ ํ๋ฉด ์ด๋:
NavigateTo<Destination> (๊ธฐ์กด URL ์ด๋์๋ NavigateUrl, NavigatePlayStore๋ ์๋ค)
- ๊ธฐ๋ณธ toast:
ShowToastMessage(val message: String) ๋๋ ShowToast(val message: String) โ ํ๋ฉด๋ง๋ค ํผ์ฌํ๋ค
- action toast:
ShowActionToast(...)
- ๊ถํ ์์ฒญ:
Request<Permission>Permission ๋๋ RequestPermission(...) (Map์ LaunchLocationPermission)
- ์ฑ ์ค์ ์ด๋:
MoveAppSettings(...) (Map์ NavigateToAppSettings)
- ์ธ๋ถ ์ฑ ์คํ:
Launch<ExternalAction>(...)
- ์ธ๋ถ ๋งํฌ ์ด๊ธฐ:
Open<ExternalTarget>(...) โ ์ผ๋ถ ํ๋ฉด์ ๋ฐํ
์ํธยท๊ฐค๋ฌ๋ฆฌ ์ด๊ธฐ์๋ ์ด๋ค
- ์ด์ ํ๋ฉด ๋ณ๊ฒฝ ํต์ง:
NotifyResult, Notify<Something>Changed
- ๊ฒฐ๊ณผ ์ ๋ฌ:
Send<X>Result, Set<X>Result
- ๋ชฉ๋ก ๊ฐฑ์ :
Refresh<Target>
Screen ์ฐ๊ฒฐ
FeatureButton(
onClick = { onIntent(FeatureIntent.ClickConfirmButton) },
)
FeatureDialog(
onDismissRequest = { onIntent(FeatureIntent.DismissDialog) },
)
ViewModel ๋ถ๊ธฐ
private fun onIntent(
intent: FeatureIntent,
state: FeatureState,
reduce: (FeatureState.() -> FeatureState) -> Unit,
postSideEffect: (FeatureEffect) -> Unit,
) {
when (intent) {
FeatureIntent.ClickConfirmButton -> submit(state, reduce, postSideEffect)
FeatureIntent.DismissDialog -> reduce { copy(isShowDialog = false) }
}
}
API ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
private fun submit(
state: FeatureState,
reduce: (FeatureState.() -> FeatureState) -> Unit,
postSideEffect: (FeatureEffect) -> Unit,
) {
viewModelScope.launch {
repository.request(...)
.onSuccess { data ->
...
}
.onFailure { e ->
Timber.e(e)
...
}
}
}
Effect ์ฒ๋ฆฌ
viewModel.store.sideEffects.collectWithLifecycle { effect ->
when (effect) {
FeatureEffect.NavigateBack -> navigateBack()
is FeatureEffect.ShowToastMessage -> nekiToast.showToast(effect.message)
}
}
ํ๋ฉด ๊ฐ ๊ฒฐ๊ณผ ์ ๋ฌ
์ด์ ํ๋ฉด ๊ฐฑ์ ์ด ํ์ํ๋ฉด LocalResultEventBus.current๋ก ์ป์ bus์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ธ๋ค.
์ก์ ํ๋ฉด Route:
FeatureSideEffect.NotifyResult -> resultEventBus.sendResult(result = FeatureResult, allowDuplicate = false)
์์ ํ๋ฉด entry:
ResultEffect<FeatureResult>(resultBus) {
viewModel.store.onIntent(OtherFeatureIntent.RefreshItems)
}