mirror of
https://github.com/bitsycore/pulse-mvi.git
synced 2026-10-05 03:57:27 +00:00
3.6 KiB
3.6 KiB
PulseLibs
Kotlin Multiplatform MVI (Model-View-Intent) library.
Project Structure
pulse/ Core MVI container - pure Kotlin + coroutines (JVM, Android, iOS, JS, WasmJS)
pulse-viewmodel/ AndroidX ViewModel integration (JVM, Android, iOS, JS, WasmJS)
pulse-savedstate/ SavedStateHandle integration - auto-persist state (JVM, Android, iOS, JS, WasmJS)
pulse-compose/ Compose Multiplatform extensions (JVM, Android, iOS, JS, WasmJS)
pulse-test/ Testing utilities - TestContainer + assertions (JVM, Android, iOS, JS, WasmJS)
demo/ Desktop demo app (JVM)
Dependency Graph
demo → pulse-compose → pulse
→ pulse-savedstate → pulse-viewmodel → pulse
→ pulse-test → pulse
pulse has zero UI dependencies - only kotlinx-coroutines-core.
Build Commands
./gradlew build # Build all modules
./gradlew :pulse:build # Build core only
./gradlew :pulse-viewmodel:build # Build viewmodel module
./gradlew :pulse-savedstate:build # Build savedstate module
./gradlew :pulse-compose:build # Build compose module
./gradlew :pulse-test:build # Build test utilities
./gradlew :demo:run # Run desktop demo app
MVI Pattern
- ContainerContract - declares
STATE,INTENT,EFFECTtypes (noinitialState; state is provided by the Container/ViewModel) - Container - core engine: takes
initialStateas constructor parameter;dispatch(intent)→reduce()→ new state;handleIntent()for async side-effects;emitEffect()for one-shot events; supportsrestoredStatefor state restoration - ContainerHost - interface exposing
stateFlow,effectFlow,dispatch - DebouncedDispatcher - standalone debounce engine:
dispatchDebounced(),cancel(key),cancelAll(),clearHistory(); thread-safe, composable with any dispatch function - ComponentContract - lightweight sub-container with its own reducer (no effects)
- PulseViewModel - AndroidX ViewModel wrapper around Container
- PulseSavedStateViewModel - PulseViewModel + SavedStateHandle auto-persistence (STATE must be
@Serializable) - ComposeExtensions -
collectAsStateWithLifecycle(),collectEffect(),collectEffectWithLifecycle(),onLifecycleIntent(),onCompositionIntent() - TestContainer - test-friendly Container with
UnconfinedTestDispatcher
Screen Pattern (Compose)
@Composable
fun XScreen(viewModel: XViewModel = viewModel { XViewModel() }) {
val state by viewModel.collectAsStateWithLifecycle()
viewModel.collectEffect { /* handle one-shot effects */ }
XContent(state, viewModel::dispatch)
}
@Composable
fun XContent(state: UiState, dispatch: (Intent) -> Unit) {
// Pure UI - no ViewModel reference
}
SavedState Pattern
@Serializable
data class UiState(val count: Int = 0)
class MyViewModel(savedStateHandle: SavedStateHandle) :
PulseSavedStateViewModel<UiState, Intent, Effect>(
containerContract = MyContract,
initialState = UiState(),
savedStateHandle = savedStateHandle,
serializer = UiState.serializer()
) {
override fun reduce(state: UiState, intent: Intent): UiState = ...
}
// In Compose:
viewModel { MyViewModel(createSavedStateHandle()) }
Testing Pattern
MyContract.containerTest(
initialState = MyContract.UiState(),
reduce = { state, intent -> /* ... */ }
) {
dispatch(MyIntent.Increment)
assertState { it.count == 1 }
}
Conventions
- Kotlin 2.3, KMP
- Tabs for indentation
- Package root:
com.bitsycore.lib.pulse - Targets: JVM, Android, iOS, JS, WasmJS