reaktiv-core
The Core module is the foundation of Reaktiv, providing the essential building blocks for the Model-View-Logic-Intent (MVLI) architecture in Kotlin Multiplatform projects.
Setup
// build.gradle.kts
dependencies {
implementation("io.github.syrou:reaktiv-core:<version>")
}Core Concepts
Store + Module
The Store is the central coordinator. Each feature domain is expressed as a Module that owns its ModuleState, a pure reducer, and a ModuleLogic factory.
// 1. Define state
data class CounterState(val count: Int = 0) : ModuleState
// 2. Define actions
sealed class CounterAction : ModuleAction() {
data object Increment : CounterAction()
data class Add(val amount: Int) : CounterAction()
}
// 3. Define logic (business logic + side effects)
class CounterLogic(val storeAccessor: StoreAccessor) : ModuleLogic() {
suspend fun increment() {
storeAccessor.dispatch(CounterAction.Increment)
}
suspend fun addDelayed(amount: Int) {
delay(500)
storeAccessor.dispatch(CounterAction.Add(amount))
}
}
// 4. Define module (wires everything together)
object CounterModule : ModuleWithLogic<CounterState, CounterAction, CounterLogic> {
override val initialState = CounterState()
override val reducer = { state: CounterState, action: CounterAction ->
when (action) {
is CounterAction.Increment -> state.copy(count = state.count + 1)
is CounterAction.Add -> state.copy(count = state.count + action.amount)
}
}
override val createLogic = { storeAccessor: StoreAccessor -> CounterLogic(storeAccessor) }
}
// 5. Create the store
val store = createStore {
module(CounterModule)
}Dispatching Actions and Reading State
// Dispatch a simple action
store.dispatch(CounterAction.Increment)
// Read the current state once
val state = store.getCurrentState<CounterState>()
// Observe state as a flow
store.selectState<CounterState>().collect { state ->
println("Count is now: ${state.count}")
}Calling Logic from Outside Compose
Use StoreAccessor.launch to call suspend methods on a ModuleLogic instance from non-Compose code (e.g. lifecycle callbacks, ViewModels, application-level events).
// From a ViewModel or lifecycle-aware component
store.launch {
val logic = store.selectLogic<CounterLogic>()
logic.addDelayed(10)
}
// Cross-module logic call (from inside another Logic class)
class CheckoutLogic(val storeAccessor: StoreAccessor) : ModuleLogic() {
suspend fun checkout() {
val orderLogic = storeAccessor.selectLogic<OrderLogic>()
orderLogic.submitOrder("order-123")
}
}Middleware
Middleware intercepts every dispatched action. Use it for logging, analytics, or side-effect orchestration. The updatedState lambda returns the module state after the action is reduced.
val loggingMiddleware = Middleware { action, updatedState ->
println("Action dispatched: $action")
val newState = updatedState(action)
println("New state: $newState")
}
val store = createStore {
module(CounterModule)
middleware(loggingMiddleware)
}State Persistence
Implement PersistenceStrategy to save and restore state across sessions. Pass it to createStore via persistWith.
class DataStorePersistence(private val dataStore: DataStore<Preferences>) : PersistenceStrategy {
override suspend fun save(key: String, value: String) {
dataStore.edit { prefs -> prefs[stringPreferencesKey(key)] = value }
}
override suspend fun load(key: String): String? {
return dataStore.data.first()[stringPreferencesKey(key)]
}
override suspend fun remove(key: String) {
dataStore.edit { prefs -> prefs.remove(stringPreferencesKey(key)) }
}
}
val store = createStore {
module(CounterModule)
persistWith(DataStorePersistence(dataStore))
}Custom Type Serializers
When a module's state contains polymorphic types or types requiring custom serializers, implement CustomTypeRegistrar so the store can build a unified SerializersModule.
@Serializable
sealed interface AppSettings
@Serializable data class BasicSettings(val theme: String) : AppSettings
@Serializable data class AdvancedSettings(val theme: String, val locale: String) : AppSettings
object SettingsModule : ModuleWithLogic<SettingsState, SettingsAction, SettingsLogic>,
CustomTypeRegistrar {
override val initialState = SettingsState(BasicSettings("dark"))
override val reducer = { state: SettingsState, action: SettingsAction -> /* ... */ state }
override val createLogic = { accessor: StoreAccessor -> SettingsLogic(accessor) }
override fun registerAdditionalSerializers(builder: SerializersModuleBuilder) {
builder.polymorphic(AppSettings::class) {
subclass(BasicSettings::class)
subclass(AdvancedSettings::class)
}
}
}Swift / iOS Interop
Reaktiv works with Swift via SKIE. Due to Kotlin type erasure across the ObjC/Swift boundary, getModule<M>() is not callable from Swift. Use one of these patterns instead.
Recommended: expose module instances as typed properties on your SDK class. This gives Swift a direct, typed reference with no store lookup required.
class AppSDK {
val navigationModule = createNavigationModule { ... }
val userModule = UserModule()
val store = createStore {
module(navigationModule)
module(userModule)
}
}Swift can then use the module's built-in interop methods:
// Observe state — non-suspend, callable directly from Swift
let stateFlow = sdk.navigationModule.selectStateFlowNonSuspend(store: store)
// Access logic — suspend, SKIE bridges this as async
let logic = try await sdk.navigationModule.selectLogicTyped(store: store)Fallback: getRegisteredModules() when a direct reference is not available. Swift can iterate the list and cast using its own type system:
let navModule = store.getRegisteredModules()
.first { $0 is NavigationModule } as? NavigationModuleKey Types
Store — central coordinator; created via
createStore { }Module / ModuleWithLogic — feature domain definition
ModuleState — immutable state marker interface
ModuleAction — action marker base class
ModuleLogic — side-effect and business logic base class
StoreAccessor — access point for
dispatch,selectState,selectLogic,launch,getModule,getRegisteredModulesMiddleware — action interception
PersistenceStrategy — save/restore state across sessions
CustomTypeRegistrar — register additional serializers for polymorphic state types