mcpField
M
mvi-architecture

mvi-architecture

github.com/ahmed3elshaer开发6642 次安装19 周前更新
代码仓库

关于 mvi-architecture

mvi-architecture 是一项介绍 Android 单向数据流架构的技能,面向 Android 应用开发者。内容涵盖状态、意图、副作用、ViewModel 与界面集成,并展示逻辑测试方式。

主要功能

  1. 01单向数据流
  2. 02状态与意图建模
  3. 03处理副作用
  4. 04ViewModel 与界面集成
  5. 05测试状态变化

适用场景

  1. 01组织 Android 页面逻辑
  2. 02建模用户操作与界面状态
  3. 03处理导航和 Snackbar 副作用
  4. 04测试 ViewModel 行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ahmed3elshaer/everything-claude-code-mobile --skill mvi-architecture

安装后在 Claude Code 中输入/mvi-architecture确认已加载。

详细介绍

MVI 架构

适用于 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
}

ViewModel

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")
        }
    }
}

UI 集成

@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
    }
}

请记住:MVI = 状态可预测、逻辑可测试、流程可调试。

评论

支持 Markdown