一键导入
mvi-architecture
Model-View-Intent architecture patterns for Android with unidirectional data flow, state management, and side effects.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Model-View-Intent architecture patterns for Android with unidirectional data flow, state management, and side effects.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Core Android development patterns for Kotlin, including coroutines, lifecycle management, and functional programming idioms.
Apple Combine framework for reactive programming. Publishers, subscribers, operators, and error handling.
Pattern extraction and skill generation for mobile development sessions. Automatically learns from your coding patterns.
Instinct-based learning system with confidence scoring for mobile patterns. Automatically extracts and evolves patterns.
Core Data for iOS persistence. Data models, fetch requests, background contexts, and SwiftData migration.
Kotlin Coroutines and Flow patterns for structured concurrency, error handling, and async operations.
| name | mvi-architecture |
| description | Model-View-Intent architecture patterns for Android with unidirectional data flow, state management, and side effects. |
Unidirectional data flow architecture for Android.
Intent → ViewModel → State → UI
↑ │
└────────────────────────┘
@Immutable
data class HomeState(
val isLoading: Boolean = false,
val items: List<Item> = emptyList(),
val error: ErrorState? = null,
val searchQuery: String = ""
) {
sealed interface ErrorState {
data class Network(val message: String) : ErrorState
data object Unauthorized : ErrorState
}
}
sealed interface HomeIntent {
object LoadItems : HomeIntent
object Refresh : HomeIntent
data class Search(val query: String) : HomeIntent
data class ItemClicked(val id: String) : HomeIntent
object ClearError : HomeIntent
}
sealed interface HomeSideEffect {
data class NavigateToDetail(val itemId: String) : HomeSideEffect
data class ShowSnackbar(val message: String) : HomeSideEffect
object NavigateToLogin : HomeSideEffect
}
class HomeViewModel(
private val getItemsUseCase: GetItemsUseCase
) : ViewModel() {
private val _state = MutableStateFlow(HomeState())
val state: StateFlow<HomeState> = _state.asStateFlow()
private val _sideEffects = Channel<HomeSideEffect>(Channel.BUFFERED)
val sideEffects: Flow<HomeSideEffect> = _sideEffects.receiveAsFlow()
fun onIntent(intent: HomeIntent) {
when (intent) {
is HomeIntent.LoadItems -> loadItems()
is HomeIntent.Refresh -> loadItems(refresh = true)
is HomeIntent.Search -> search(intent.query)
is HomeIntent.ItemClicked -> {
viewModelScope.launch {
_sideEffects.send(HomeSideEffect.NavigateToDetail(intent.id))
}
}
is HomeIntent.ClearError -> _state.update { it.copy(error = null) }
}
}
private fun loadItems(refresh: Boolean = false) {
viewModelScope.launch {
if (!refresh) _state.update { it.copy(isLoading = true) }
getItemsUseCase()
.onSuccess { items ->
_state.update { it.copy(isLoading = false, items = items, error = null) }
}
.onFailure { error ->
_state.update { it.copy(isLoading = false, error = mapError(error)) }
}
}
}
private fun mapError(error: Throwable): HomeState.ErrorState {
return when (error) {
is UnauthorizedException -> HomeState.ErrorState.Unauthorized
else -> HomeState.ErrorState.Network(error.message ?: "Unknown error")
}
}
}
@Composable
fun HomeScreen(
viewModel: HomeViewModel = koinViewModel(),
onNavigateToDetail: (String) -> Unit,
onNavigateToLogin: () -> Unit
) {
val state by viewModel.state.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
// Handle side effects
LaunchedEffect(Unit) {
viewModel.sideEffects.collect { effect ->
when (effect) {
is HomeSideEffect.NavigateToDetail -> onNavigateToDetail(effect.itemId)
is HomeSideEffect.ShowSnackbar -> snackbarHostState.showSnackbar(effect.message)
is HomeSideEffect.NavigateToLogin -> onNavigateToLogin()
}
}
}
// Load data
LaunchedEffect(Unit) {
viewModel.onIntent(HomeIntent.LoadItems)
}
HomeContent(
state = state,
onIntent = viewModel::onIntent,
snackbarHostState = snackbarHostState
)
}
@Composable
private fun HomeContent(
state: HomeState,
onIntent: (HomeIntent) -> Unit,
snackbarHostState: SnackbarHostState
) {
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
when {
state.isLoading -> LoadingIndicator()
state.error != null -> ErrorContent(
error = state.error,
onRetry = { onIntent(HomeIntent.LoadItems) }
)
else -> ItemList(
items = state.items,
onItemClick = { onIntent(HomeIntent.ItemClicked(it)) }
)
}
}
}
@Test
fun `when LoadItems succeeds, state contains items`() = runTest {
val items = listOf(Item("1", "Test"))
coEvery { getItemsUseCase() } returns Result.success(items)
viewModel.state.test {
awaitItem() // Initial
viewModel.onIntent(HomeIntent.LoadItems)
awaitItem().isLoading shouldBe true
awaitItem().items shouldBe items
}
}
Remember: MVI = predictable state, testable logic, debuggable flow.