creating-feature-screen
Use when adding a new Compose feature screen with Contract, ViewModel, Route, Screen, Preview, or MVI store in this Android repository.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when adding a new Compose feature screen with Contract, ViewModel, Route, Screen, Preview, or MVI store in this Android repository.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when naming branches, writing commit messages, creating or editing GitHub issues, or creating or editing GitHub pull requests in this repository.
Use when a project decision changes or the codebase drifts from what a skill or agent documents, and this repository's harness (skills, agents, AGENTS.md, discussions.md) needs to be updated to match.
Use after implementing an Android code change, when verifying an existing implementation, or before declaring development complete. Select project skills, run risk-based verification, coordinate Reviewer and QA agents, and repeat only when evidence requires a fix.
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.
Use when adding or changing Firebase Analytics events, AnalyticsEvent definitions, AnalyticsLogger calls, screen view logs, click logs, filter logs, or completion logs in this Android repository.
Use when adding or changing DataStore Preferences, local flags, token/auth/user persistence, preference keys, or local repository methods in this Android repository.
| 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. |
새 화면은 <Feature>Contract.kt, <Feature>ViewModel.kt, <Feature>Screen.kt를 같은 화면 이름으로 만든다.
feature/<feature-name>/impl/src/main/.../<Feature>Contract.ktfeature/<feature-name>/impl/src/main/.../<Feature>ViewModel.ktfeature/<feature-name>/impl/src/main/.../<Feature>Screen.ktfeature/<feature-name>/api/src/main/.../<Feature>NavKey.ktfeature/<feature-name>/impl/src/main/.../navigation/<Feature>EntryProvider.kt다중 화면 feature는 impl/<screen>/ 서브패키지에 Contract/ViewModel/Screen을 두고, 화면 전용 컴포넌트는 impl/<screen>/component/에 둔다.
State, Intent, Effect를 만든다.MviIntentStore를 만든다.Route, Screen, private preview를 둔다.<Feature>NavKey에 @Serializable NavKey와 MainNavigator.navigateToXxx() 확장을 추가한다.entry<NavKey> { ...Route(...) }를 등록한다. 등록하지 않으면 화면에 도달할 수 없다.data class FeatureState(
val isLoading: Boolean = false,
)
sealed interface FeatureIntent {
data object ClickBackIcon : FeatureIntent
}
sealed interface FeatureEffect {
data object NavigateBack : FeatureEffect
}
기존 코드에는 SideEffect(다수)와 Effect 접미사가 혼재한다. 기존 화면을 수정할 때는 해당 화면의 접미사를 따른다.
@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) },
)
@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는 이 레포에 의존성이 없어 컴파일되지 않는다.
@Composable
internal fun FeatureScreen(
uiState: FeatureState,
onIntent: (FeatureIntent) -> Unit = {},
) {
...
}
@DevicePreview
@Composable
private fun FeatureScreenPreview() {
NekiTheme {
FeatureScreen(
uiState = FeatureState(),
onIntent = {},
)
}
}
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,
)
}
}
}
settings.gradle.kts에 include(":feature:<name>:api"), include(":feature:<name>:impl")를 등록한다.build.gradle.kts에 alias(libs.plugins.neki.android.feature.api)와 namespace를 선언한다.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를 자동으로 추가한다.