# Flow Patterns in Amethyst StateFlow and SharedFlow usage patterns from the codebase. ## Table of Contents - [StateFlow for State Management](#stateflow-for-state-management) - [Flow Composition](#flow-composition) - [Common Patterns](#common-patterns) - [Anti-Patterns](#anti-patterns) --- ## StateFlow for State Management ### AccountManager Pattern **File:** `commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt:36-115` ```kotlin sealed class AccountState { data object LoggedOut : AccountState() data class LoggedIn( val signer: NostrSigner, val pubKeyHex: String, val npub: String, val nsec: String?, val isReadOnly: Boolean, ) : AccountState() } class AccountManager { private val _accountState = MutableStateFlow(AccountState.LoggedOut) val accountState: StateFlow = _accountState.asStateFlow() fun generateNewAccount(): AccountState.LoggedIn { val keyPair = KeyPair() val signer = NostrSignerInternal(keyPair) val state = AccountState.LoggedIn( signer = signer, pubKeyHex = keyPair.pubKey.toHexKey(), npub = keyPair.pubKey.toNpub(), nsec = keyPair.privKey?.toNsec(), isReadOnly = false ) _accountState.value = state // Update state return state } fun loginWithKey(keyInput: String): Result { // ... validation ... val state = AccountState.LoggedIn(...) _accountState.value = state return Result.success(state) } fun logout() { _accountState.value = AccountState.LoggedOut } } ``` **Pattern highlights:** - Private `MutableStateFlow` for internal mutations - Public `StateFlow` via `.asStateFlow()` for read-only access - Sealed class for type-safe state variants - Initial value required (`AccountState.LoggedOut`) ### RelayConnectionManager Pattern **File:** `commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt:44-80` ```kotlin data class RelayStatus( val url: NormalizedRelayUrl, val connected: Boolean, val error: String? = null, val messageCount: Int = 0 ) open class RelayConnectionManager( websocketBuilder: WebsocketBuilder ) : IRelayClientListener { private val client = NostrClient(websocketBuilder) // Map of relay URLs to their status private val _relayStatuses = MutableStateFlow>(emptyMap()) val relayStatuses: StateFlow> = _relayStatuses.asStateFlow() // Delegated StateFlows from client val connectedRelays: StateFlow> = client.connectedRelaysFlow() val availableRelays: StateFlow> = client.availableRelaysFlow() fun addRelay(url: String): NormalizedRelayUrl? { val normalized = RelayUrlNormalizer.normalizeOrNull(url) ?: return null updateRelayStatus(normalized) { it.copy(connected = false, error = null) } return normalized } fun removeRelay(url: NormalizedRelayUrl) { _relayStatuses.value = _relayStatuses.value - url // Immutable update (remove from map) } private fun updateRelayStatus( relay: NormalizedRelayUrl, update: (RelayStatus) -> RelayStatus ) { _relayStatuses.value = _relayStatuses.value.toMutableMap().apply { val current = get(relay) ?: RelayStatus(relay, false) put(relay, update(current)) } } // IRelayClientListener implementation override fun onConnect(relay: NormalizedRelayUrl) { updateRelayStatus(relay) { it.copy(connected = true, error = null) } } override fun onError(relay: NormalizedRelayUrl, error: String) { updateRelayStatus(relay) { it.copy(connected = false, error = error) } } } ``` **Pattern highlights:** - `Map` as state value for collection tracking - Immutable map updates (copy with modifications) - Helper function `updateRelayStatus` for consistent updates - Delegation pattern (client exposes its own StateFlows) --- ## Flow Composition ### Multiple StateFlows in UI **Pattern:** ```kotlin @Composable fun LoginScreen(accountManager: AccountManager) { val accountState by accountManager.accountState.collectAsState() when (accountState) { is AccountState.LoggedOut -> { LoginForm(onLogin = { key -> accountManager.loginWithKey(key) }) } is AccountState.LoggedIn -> { MainApp(account = accountState as AccountState.LoggedIn) } } } ``` ### Observing Multiple Flows **Pattern:** ```kotlin @Composable fun RelayStatusCard(relayManager: RelayConnectionManager) { val relayStatuses by relayManager.relayStatuses.collectAsState() val connectedRelays by relayManager.connectedRelays.collectAsState() Column { Text("${connectedRelays.size} of ${relayStatuses.size} relays connected") relayStatuses.forEach { (url, status) -> RelayRow( url = url, connected = status.connected, error = status.error ) } } } ``` --- ## Common Patterns ### Pattern: Immutable State Updates ```kotlin // Map updates _relayStatuses.value = _relayStatuses.value + (url to newStatus) // Add _relayStatuses.value = _relayStatuses.value - url // Remove _relayStatuses.value = _relayStatuses.value.mapValues { (key, value) -> if (key == targetUrl) value.copy(connected = true) else value } // List updates _items.value = _items.value + newItem // Append _items.value = _items.value.filter { it.id != removedId } // Remove _items.value = _items.value.map { if (it.id == id) it.copy(name = newName) else it } // Update // Object updates _user.value = _user.value.copy(name = newName) ``` ### Pattern: Conditional State Transitions ```kotlin fun attemptLogin(credentials: Credentials) { if (_loginState.value is LoginState.LoggingIn) { return // Already logging in, ignore } _loginState.value = LoginState.LoggingIn viewModelScope.launch { try { val user = repository.login(credentials) _loginState.value = LoginState.Success(user) } catch (e: Exception) { _loginState.value = LoginState.Error(e.message ?: "Login failed") } } } ``` ### Pattern: Derived State ```kotlin class MyViewModel { private val _items = MutableStateFlow>(emptyList()) val items: StateFlow> = _items.asStateFlow() // Derived state (computed from items) val itemCount: StateFlow = items.map { it.size } .stateIn(viewModelScope, SharingStarted.Lazily, 0) val hasItems: StateFlow = items.map { it.isNotEmpty() } .stateIn(viewModelScope, SharingStarted.Lazily, false) } // Usage in Compose @Composable fun ItemList(viewModel: MyViewModel) { val itemCount by viewModel.itemCount.collectAsState() val hasItems by viewModel.hasItems.collectAsState() if (hasItems) { Text("$itemCount items") } else { Text("No items") } } ``` ### Pattern: State with Loading/Error ```kotlin sealed class UiState { data object Loading : UiState() data class Success(val data: T) : UiState() data class Error(val message: String) : UiState() } class FeedViewModel { private val _feedState = MutableStateFlow>>(UiState.Loading) val feedState: StateFlow>> = _feedState.asStateFlow() fun loadFeed() { viewModelScope.launch { _feedState.value = UiState.Loading try { val events = repository.getEvents() _feedState.value = UiState.Success(events) } catch (e: Exception) { _feedState.value = UiState.Error(e.message ?: "Unknown error") } } } } // UI @Composable fun FeedScreen(viewModel: FeedViewModel) { val state by viewModel.feedState.collectAsState() when (state) { is UiState.Loading -> LoadingSpinner() is UiState.Success -> EventList((state as UiState.Success).data) is UiState.Error -> ErrorMessage((state as UiState.Error).message) } } ``` --- ## Anti-Patterns ### ❌ Exposing Mutable State ```kotlin // BAD: External code can mutate class BadViewModel { val state: MutableStateFlow = MutableStateFlow(State.Initial) } // Caller can do: viewModel.state.value = State.Hacked // Bypass internal logic! ``` ### ✅ Expose Immutable ```kotlin // GOOD: Only ViewModel can mutate class GoodViewModel { private val _state = MutableStateFlow(State.Initial) val state: StateFlow = _state.asStateFlow() fun updateState(newState: State) { // Controlled mutation with validation _state.value = newState } } ``` --- ### ❌ Not Using Immutable Updates ```kotlin // BAD: Mutating collection doesn't trigger StateFlow update val list = mutableListOf() list.add(newItem) _items.value = list // Same reference, no update emitted! ``` ### ✅ Create New Instance ```kotlin // GOOD: New list instance _items.value = _items.value + newItem // New list created, update emitted ``` --- ### ❌ StateFlow for Events ```kotlin // BAD: Events get lost if no collector class BadViewModel { val navigationEvent: StateFlow = MutableStateFlow(null) fun navigate(event: NavEvent) { _navigationEvent.value = event // Lost if UI not observing! } } ``` ### ✅ SharedFlow for Events ```kotlin // GOOD: Events queued class GoodViewModel { private val _navigationEvent = MutableSharedFlow(replay = 0) val navigationEvent: SharedFlow = _navigationEvent.asSharedFlow() fun navigate(event: NavEvent) { viewModelScope.launch { _navigationEvent.emit(event) // Queued for collector } } } ``` --- ### ❌ Blocking Operations in State Update ```kotlin // BAD: Blocking main thread fun loadData() { _state.value = fetchDataFromNetwork() // Blocks! } ``` ### ✅ Async Updates ```kotlin // GOOD: Use coroutines fun loadData() { viewModelScope.launch { _state.value = UiState.Loading val data = withContext(Dispatchers.IO) { fetchDataFromNetwork() } _state.value = UiState.Success(data) } } ``` --- ## References - AccountManager.kt:36-115 - RelayConnectionManager.kt:44-80 - [StateFlow and SharedFlow | Android Developers](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow) - [Hot vs Cold Flows](https://carrion.dev/en/posts/kotlin-flows-hot-cold/)