mirror of
https://github.com/bitsycore/pulse-mvi.git
synced 2026-10-05 03:57:27 +00:00
104 lines
3.6 KiB
Markdown
104 lines
3.6 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
./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`, `EFFECT` types (no `initialState`; state is provided by the Container/ViewModel)
|
|
- **Container** - core engine: takes `initialState` as constructor parameter; `dispatch(intent)` → `reduce()` → new state; `handleIntent()` for async side-effects; `emitEffect()` for one-shot events; supports `restoredState` for 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)
|
|
|
|
```kotlin
|
|
@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
|
|
|
|
```kotlin
|
|
@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
|
|
|
|
```kotlin
|
|
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
|