| name | creating-feature-screen |
| description | Use when adding a new Compose feature screen with Contract, ViewModel, Route, Screen, Preview, or MVI store in this Android repository. |
Creating Feature Screen
์ ํ๋ฉด์ <Feature>Contract.kt, <Feature>ViewModel.kt, <Feature>Screen.kt๋ฅผ ๊ฐ์ ํ๋ฉด ์ด๋ฆ์ผ๋ก ๋ง๋ ๋ค.
์์
๋ฒ์
feature/<feature-name>/impl/src/main/.../<Feature>Contract.kt
feature/<feature-name>/impl/src/main/.../<Feature>ViewModel.kt
feature/<feature-name>/impl/src/main/.../<Feature>Screen.kt
feature/<feature-name>/api/src/main/.../<Feature>NavKey.kt
feature/<feature-name>/impl/src/main/.../navigation/<Feature>EntryProvider.kt
๋ค์ค ํ๋ฉด feature๋ impl/<screen>/ ์๋ธํจํค์ง์ Contract/ViewModel/Screen์ ๋๊ณ , ํ๋ฉด ์ ์ฉ ์ปดํฌ๋ํธ๋ impl/<screen>/component/์ ๋๋ค.
๊ตฌํ ํ๋ฆ
- Contract์
State, Intent, Effect๋ฅผ ๋ง๋ ๋ค.
- ViewModel์
MviIntentStore๋ฅผ ๋ง๋ ๋ค.
- Screen ํ์ผ์
Route, Screen, private preview๋ฅผ ๋๋ค.
- api ๋ชจ๋์
<Feature>NavKey์ @Serializable NavKey์ MainNavigator.navigateToXxx() ํ์ฅ์ ์ถ๊ฐํ๋ค.
- impl ๋ชจ๋์ EntryProvider์
entry<NavKey> { ...Route(...) }๋ฅผ ๋ฑ๋กํ๋ค. ๋ฑ๋กํ์ง ์์ผ๋ฉด ํ๋ฉด์ ๋๋ฌํ ์ ์๋ค.
Contract
data class FeatureState(
val isLoading: Boolean = false,
)
sealed interface FeatureIntent {
data object ClickBackIcon : FeatureIntent
}
sealed interface FeatureEffect {
data object NavigateBack : FeatureEffect
}
๊ธฐ์กด ์ฝ๋์๋ SideEffect(๋ค์)์ Effect ์ ๋ฏธ์ฌ๊ฐ ํผ์ฌํ๋ค. ๊ธฐ์กด ํ๋ฉด์ ์์ ํ ๋๋ ํด๋น ํ๋ฉด์ ์ ๋ฏธ์ฌ๋ฅผ ๋ฐ๋ฅธ๋ค.
ViewModel
@HiltViewModel
internal class FeatureViewModel @Inject constructor() : ViewModel() {
val store: MviIntentStore<FeatureState, FeatureIntent, FeatureEffect> =
mviIntentStore(
initialState = FeatureState(),
onIntent = ::onIntent,
)
private fun onIntent(
intent: FeatureIntent,
state: FeatureState,
reduce: (FeatureState.() -> FeatureState) -> Unit,
postSideEffect: (FeatureEffect) -> Unit,
) {
when (intent) {
FeatureIntent.ClickBackIcon -> postSideEffect(FeatureEffect.NavigateBack)
}
}
}
ํ๋ฉด ์ง์
์ ๋ฐ์ดํฐ ๋ก๋ฉ์ด ํ์ํ๋ฉด initialFetchData๋ฅผ ์ฌ์ฉํ๋ค.
mviIntentStore(
initialState = FeatureState(),
onIntent = ::onIntent,
initialFetchData = { store.onIntent(FeatureIntent.EnterFeatureScreen) },
)
Route
@Composable
internal fun FeatureRoute(
viewModel: FeatureViewModel = hiltViewModel(),
navigateBack: () -> Unit,
) {
val uiState by viewModel.store.uiState.collectAsStateWithLifecycle()
viewModel.store.sideEffects.collectWithLifecycle { effect ->
when (effect) {
FeatureEffect.NavigateBack -> navigateBack()
}
}
FeatureScreen(
uiState = uiState,
onIntent = viewModel.store::onIntent,
)
}
hiltViewModel()์ androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel์ importํ๋ค. androidx.hilt.navigation.compose๋ ์ด ๋ ํฌ์ ์์กด์ฑ์ด ์์ด ์ปดํ์ผ๋์ง ์๋๋ค.
Screen
@Composable
internal fun FeatureScreen(
uiState: FeatureState,
onIntent: (FeatureIntent) -> Unit = {},
) {
...
}
Preview
@DevicePreview
@Composable
private fun FeatureScreenPreview() {
NekiTheme {
FeatureScreen(
uiState = FeatureState(),
onIntent = {},
)
}
}
EntryProvider ๋ฑ๋ก
impl ๋ชจ๋์ navigation/<Feature>EntryProvider.kt์ Hilt ๋ชจ๋๋ก entry๋ฅผ ๋ฑ๋กํ๋ค.
@Module
@InstallIn(ActivityRetainedComponent::class)
object FeatureEntryProviderModule {
@IntoSet
@Provides
fun provideFeatureEntryBuilder(
mainNavigator: MainNavigator,
): EntryProviderInstaller = {
entry<FeatureNavKey.Feature> {
FeatureRoute(
navigateBack = mainNavigator::goBack,
)
}
}
}
์ feature ๋ชจ๋ ์์ฑ (๊ธฐ์กด ๋ชจ๋์ ํ๋ฉด๋ง ์ถ๊ฐํ๋ฉด ์๋ต)
settings.gradle.kts์ include(":feature:<name>:api"), include(":feature:<name>:impl")๋ฅผ ๋ฑ๋กํ๋ค.
- api
build.gradle.kts์ alias(libs.plugins.neki.android.feature.api)์ namespace๋ฅผ ์ ์ธํ๋ค.
- impl
build.gradle.kts์ alias(libs.plugins.neki.android.feature.impl), namespace, implementation(projects.feature.<name>.api)๋ฅผ ์ ์ธํ๋ค.
app/build.gradle.kts์ api, impl ๋ชจ๋์ ๋ชจ๋ implementation์ผ๋ก ๋ฑ๋กํ๋ค.
impl convention plugin์ด core:designsystem, core:ui, core:data-api, core:common, core:domain, core:analytics์ hilt๋ฅผ ์๋์ผ๋ก ์ถ๊ฐํ๋ค.