observableStateContainer

fun <T> observableStateContainer(initialState: T, validator: StateValidator<T> = NoOpValidator()): ObservableStateContainer<T>

Creates an observable state container that supports state change notifications.

This wraps a basic state container with observer capabilities, allowing you to react to state changes using the observer pattern. Perfect for implementing reactive architectures, UI updates, and testing state changes.

Basic Usage Example:

data class CounterState(val count: Int = 0)

val container = observableStateContainer(CounterState())

container.observe { oldState, newState ->
println("Count changed from ${oldState.count} to ${newState.count}")
}

container.updateState { it.copy(count = it.count + 1) }

Testing Patterns and Examples:

import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
import kotlin.test.*

// Test state for user profile management
data class UserProfileState(
val name: String = "",
val email: String = "",
val isVerified: Boolean = false,
val lastUpdated: Long = 0
)

// Comprehensive test suite for state container
class StateContainerTest {

@Test
fun `should update state and notify observers`() = runTest {
// Arrange
val container = observableStateContainer(UserProfileState())
val stateChanges = mutableListOf<Pair<UserProfileState, UserProfileState>>()

container.observe { oldState, newState ->
stateChanges.add(oldState to newState)
}

// Act
container.updateState { it.copy(name = "John Doe") }
container.updateState { it.copy(email = "john@example.com") }

// Assert
assertEquals(2, stateChanges.size)
assertEquals("John Doe", stateChanges[0].second.name)
assertEquals("john@example.com", stateChanges[1].second.email)
}

@Test
fun `should handle concurrent state updates safely`() = runTest {
// Arrange
val container = observableStateContainer(UserProfileState())
val updateResults = mutableListOf<StateUpdateResult<UserProfileState>>()

// Act - Launch multiple concurrent updates
val jobs = (1..100).map { i ->
launch {
val result = container.updateState { state ->
state.copy(name = "User$i", lastUpdated = i.toLong())
}
synchronized(updateResults) {
updateResults.add(result)
}
}
}

jobs.joinAll()

// Assert
assertEquals(100, updateResults.size)
assertTrue(updateResults.all { it is StateUpdateResult.Success })
assertTrue(container.getCurrentState().lastUpdated > 0)
}

@Test
fun `should validate state updates and reject invalid states`() = runTest {
// Arrange
val validator = StateValidator<UserProfileState> { state ->
when {
state.name.isBlank() -> ValidationResult.failure("Name cannot be blank")
!state.email.contains("@") && state.email.isNotEmpty() ->
ValidationResult.failure("Invalid email format")
else -> ValidationResult.success()
}
}

val container = observableStateContainer(
UserProfileState(name = "Valid User"),
validator
)

val rejectedUpdates = mutableListOf<StateUpdateResult.ValidationFailure<UserProfileState>>()

// Act
val result1 = container.updateState { it.copy(name = "") }
val result2 = container.updateState { it.copy(email = "invalid-email") }
val result3 = container.updateState { it.copy(email = "valid@email.com") }

// Assert
assertTrue(result1 is StateUpdateResult.ValidationFailure)
assertTrue(result2 is StateUpdateResult.ValidationFailure)
assertTrue(result3 is StateUpdateResult.Success)

assertEquals("Valid User", container.getCurrentState().name)
assertEquals("valid@email.com", container.getCurrentState().email)
}

@Test
fun `should support Flow-based state observation for reactive testing`() = runTest {
// Arrange
val container = observableStateContainer(UserProfileState())
val stateFlow = container.asFlow()
val collectedStates = mutableListOf<UserProfileState>()

// Act
val job = launch {
stateFlow.take(4).collect { state ->
collectedStates.add(state)
}
}

delay(10) // Let collection start

container.updateState { it.copy(name = "Alice") }
container.updateState { it.copy(email = "alice@example.com") }
container.updateState { it.copy(isVerified = true) }

job.join()

// Assert
assertEquals(4, collectedStates.size)
assertEquals(UserProfileState(), collectedStates[0]) // Initial state
assertEquals("Alice", collectedStates[1].name)
assertEquals("alice@example.com", collectedStates[2].email)
assertTrue(collectedStates[3].isVerified)
}
}

// Extension function for Flow-based observation (useful for testing and reactive UI)
fun <T> ObservableStateContainer<T>.asFlow(): Flow<T> = callbackFlow {
val observer = StateObserver<T> { _, newState ->
trySend(newState)
}

addObserver(observer)
trySend(getCurrentState()) // Emit current state immediately

awaitClose {
removeObserver(observer)
}
}

// Mock test utilities for integration testing
class MockUserService {
suspend fun updateUserProfile(name: String, email: String): Result<Unit> {
delay(100) // Simulate network call
return if (name.isNotBlank() && email.contains("@")) {
Result.success(Unit)
} else {
Result.failure(IllegalArgumentException("Invalid user data"))
}
}
}

// Integration test showing real-world usage with external services
class UserProfileManagerTest {

@Test
fun `should update profile through service and reflect changes in state`() = runTest {
// Arrange
val container = observableStateContainer(UserProfileState())
val userService = MockUserService()
val stateHistory = mutableListOf<UserProfileState>()

container.observe { _, newState ->
stateHistory.add(newState)
}

// Act
val updateResult = userService.updateUserProfile("Alice", "alice@example.com")

if (updateResult.isSuccess) {
container.updateState {
it.copy(
name = "Alice",
email = "alice@example.com",
lastUpdated = System.currentTimeMillis()
)
}
}

// Assert
assertTrue(updateResult.isSuccess)
assertEquals(1, stateHistory.size)
assertEquals("Alice", stateHistory[0].name)
assertEquals("alice@example.com", stateHistory[0].email)
assertTrue(stateHistory[0].lastUpdated > 0)
}
}

Return

A new ObservableStateContainer instance

Parameters

T

The type of state being managed

initialState

The initial state of the container

validator

Optional validator for state validation (defaults to NoOpValidator)