agentsclimarketplace

Mvi pattern

Skill thetruong1099/android-mvi-base-code/.claude/skills/mvi-pattern

Implements MVI (Model-View-Intent) pattern for Android feature development. Use when creating new feature ViewModels, defining ViewState/ViewEvent/ViewEffect, handling data loading with callPagingDataWithInternet or collectDataStateWithInternet, structuring Screen composables with stateful/stateless split, or handling navigation via effects.From its SKILL.md

Install
npx -y skills add thetruong1099/android-mvi-base-code --skill mvi-pattern

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

7.2 KB, ~1.5k tokens by cl100k_base, as published. Nobody here has run it

MVI Pattern

Overview

User Action -> Event -> onTriggerEvent() -> setState{} / setEffect() -> UI Update

Core Contracts

interface IViewState      // Marker for UI state (data class)
interface IViewEvent      // Marker for user actions (sealed interface)
interface IViewEffect     // Marker for one-time side effects (sealed interface)

BaseViewModel API

abstract class BaseViewModel<State : IViewState, Event : IViewEvent, Effect : IViewEffect>
    : ViewModel(), IBaseViewModel<State, Event, Effect>

Abstract Methods (MUST implement)

abstract fun createInitialState(): State
abstract fun onTriggerEvent(event: Event)

State Management

protected fun setState(reduce: State.() -> State)  // Update with reducer
protected val currentState: State                   // Access current state

Effect Management

protected fun setEffect(effect: Effect)  // Trigger one-time effect
fun clearEffect()                        // Clear after consumption

Loading & Pull-to-Refresh

protected fun onLoading(loading: Boolean = true)
protected fun onEnablePullRefresh(enable: Boolean = true)

Toast

protected fun showErrorToast(error: AppError)
protected fun showSuccessToast(messageResId: Int)
protected fun showWarningToast(messageResId: Int)
protected fun showInfoToast(messageResId: Int)

Data Collection Functions

// PagingData (returns Flow - NOT suspend)
protected fun <T : Any> callPagingDataWithInternet(
    callFlow: () -> Flow<PagingData<T>>,
    onError: (AppError) -> Unit = {}
): Flow<PagingData<T>>

// DataState flows (suspend - terminal operation)
protected suspend fun <T> collectDataStateWithInternet(
    callFlow: Flow<DataState<T>>,
    onSuccess: suspend (T) -> Unit,
    onError: suspend (AppError) -> Unit = {},
    showLoading: Boolean = true,
)

// One-shot suspend operations
protected suspend fun <T> callSuspendWithInternet(
    operation: suspend () -> T,
    onSuccess: suspend (T) -> Unit = {},
    onError: suspend (AppError) -> Unit = {},
    showLoading: Boolean = false,
)

// Variants without internet check
protected fun <T : Any> callPagingDataWithoutInternet(...)
protected suspend fun <T> collectDataState(...)
protected suspend fun <T> callSuspendWithoutInternet(...)

Complete Feature Example

1. ViewModel

@HiltViewModel
class SampleViewModel @Inject constructor(
    private val getSampleUseCase: GetSampleUseCase,
) : BaseViewModel<SampleViewState, SampleViewEvent, SampleViewEffect>() {

    override fun createInitialState(): SampleViewState = SampleViewState()

    override fun onTriggerEvent(event: SampleViewEvent) {
        when (event) {
            is SampleViewEvent.OnItemClick ->
                setEffect(SampleViewEffect.NavigateToDetail(event.item.id))
            is SampleViewEvent.OnRefresh ->
                loadData()
        }
    }

    init {
        onEnablePullRefresh(true)
        loadData()
    }

    private fun loadData() {
        val result = callPagingDataWithInternet(
            callFlow = { getSampleUseCase() },
            onError = { error -> showErrorToast(error) },
        ).cachedIn(viewModelScope)
        setState { copy(items = result) }
    }
}

data class SampleViewState(
    val items: Flow<PagingData<SampleModel>>? = null,
) : IViewState

sealed interface SampleViewEvent : IViewEvent {
    data class OnItemClick(val item: SampleModel) : SampleViewEvent
    data object OnRefresh : SampleViewEvent
}

sealed interface SampleViewEffect : IViewEffect {
    data class NavigateToDetail(val id: String) : SampleViewEffect
}

2. Screen

// Stateful: handles DI, navigation, effects
@Composable
fun SampleScreen(viewModel: SampleViewModel = hiltViewModel()) {
    val navController = LocalAppNavController.current

    LaunchedEffect(Unit) {
        viewModel.effectState.collect { effect ->
            when (effect) {
                is SampleViewEffect.NavigateToDetail -> {
                    navController.navigateTo(Screen.DetailScreen(id = effect.id))
                    viewModel.clearEffect()
                }
                null -> {}
            }
        }
    }

    SampleScreenInternal(
        viewModel = viewModel,
        onItemClick = { item -> viewModel.onTriggerEvent(SampleViewEvent.OnItemClick(item)) },
    )
}

// Stateless: previewable, uses IBaseViewModel interface
@Composable
internal fun SampleScreenInternal(
    viewModel: IBaseViewModel<SampleViewState, SampleViewEvent, SampleViewEffect>,
    modifier: Modifier = Modifier,
    onItemClick: (SampleModel) -> Unit = {},
) {
    BaseScreenComponent(
        viewModel = viewModel,
        modifier = modifier.fillMaxSize(),
    ) { state, paddingValues ->
        // UI content using state
    }
}

3. Navigation

fun NavGraphBuilder.sampleScreen() {
    composable<Screen.SampleScreen> { SampleScreen() }
}

4. Preview

@Preview(showBackground = true)
@Composable
private fun SampleScreenPreview() {
    val fakeViewModel = FakeBaseViewModel<SampleViewState, SampleViewEvent, SampleViewEffect>(
        initialState = SampleViewState()
    )
    TemplateThemePreview(darkTheme = false) {
        SampleScreenInternal(viewModel = fakeViewModel)
    }
}

CORRECT vs INCORRECT

// State: use setState with copy()
setState { copy(isLoading = true, items = newItems) }       // CORRECT
_uiState.value = currentState.copy(isLoading = true)        // INCORRECT

// Events: all user actions through events
viewModel.onTriggerEvent(SampleViewEvent.OnRefresh)          // CORRECT
viewModel.refresh()                                          // INCORRECT - don't expose methods

// Effects: navigation through effects, not state
setEffect(MyEffect.NavigateToDetail(event.id))               // CORRECT
setState { copy(navigateTo = "detail") }                     // INCORRECT

// PagingData: callPagingDataWithInternet returns Flow (NOT suspend)
val result = callPagingDataWithInternet(callFlow = { useCase() }, ...).cachedIn(viewModelScope)
viewModelScope.launch { callPagingDataWithInternet(...) }    // INCORRECT - don't launch Flow

// DataState: suspend, launch in viewModelScope
viewModelScope.launch { collectDataStateWithInternet(...) }  // CORRECT

Composition Managers (Internal)

ManagerExposed via
StateManagersetState {}, currentState
EffectManagersetEffect(), clearEffect()
LoadingStateManageronLoading(), onEnablePullRefresh()
ToastManagershowErrorToast(), showSuccessToast(), etc.
FlowCollectionManagercallPagingData*(), collectDataState*(), etc.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.