In a kotlin multiplatform app I want for the desktop application to handle the "navigation" between different panes without the help of any Library.
I have one window with two panes:
- the first pane displays a list of names
- the second pane is empty
When a name is clicked in the list of the first pane I want to display it in the second pane.
In order to do that I have implemented a singleton that exposes a data class that represent the state of the screens. The data class is:
data class ScreensState(
var paneTwo: String = ""
)
The singleton is:
object SingletonNav {
private val _viewState: MutableStateFlow<ScreensState> = MutableStateFlow(ScreensState())
val viewState: StateFlow<ScreensState> = _viewState.asStateFlow()
fun updatePaneTwoState(strg: String) {
viewState.value.paneTwo = strg
}
}
In the main app.kt
function the panes are set as follow, and a click handler is set to emit a new :
internal fun App(platform: String) {
val paneViewModel = PaneOneViewModel()
val paneTwoViewModel = PaneTwoViewModel()
Row(Modifier.fillMaxSize()) {
Box(modifier = Modifier.fillMaxWidth(0.2f), contentAlignment = Alignment.Center) {
PaneOne(
paneViewModel,
nameSelected = { // The click handler to update ScreensState
SingletonNav.updatePaneTwoState(it)
println("App | ClickHandler Emit new string!!: $it (${SingletonNav.getPaneTwoState()})")
})
}
Divider(
color = Color.LightGray,
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
)
Spacer(Modifier.width(4.dp))
Box(modifier = Modifier.fillMaxWidth(0.3f), contentAlignment = Alignment.Center) {
PaneTwo(paneTwoViewModel)
}
}
}
In the second pane´s ViewModel I collect the ScreensState
, when it changes I update the state of the second Pane to display the name clicked in the first pane:
class PaneTwoViewModel{
private val _uiState = MutableStateFlow(PaneTwoUiState())
val uiState: StateFlow<PaneTwoUiState> = _uiState.asStateFlow()
val defaultDispatcher = Dispatchers.Default
val emptyParentJob = Job()
val combinedContext = defaultDispatcher + emptyParentJob
private val scope = CoroutineScope(combinedContext)
init{
scope.launch(context = combinedContext) {
observeNavigationEvent()
}
}
private suspend fun observeNavigationEvent(){
println("PaneTwoViewModel | observeNavigationEvent")
SingletonNav.viewState.collect{
updatePaneTwo(it.paneTwo)
}
}
/**
* Update the state of the second pane to display name on the second pane
**/
private fun updatePaneTwo(name: String = ""){
if (name.isNotEmpty()){
println("PaneTwoViewModel | updatePaneTwo | collected ${name} ")
uiState.value.nameToDisplay = name
}
}
}
The problem is that the second pane is never updated.
In the log I can see that the name is emitted, but it is not collected:
#logs printed:
NamesScreen | CLICKED on Nathan
App | ClickHandler Emit new string!!: Nathan (Nathan)
Once the new string is emitted I expect to see the log of the updatePaneTwo
function and to see the second pane displays the name, but nothing happens.
What is wrong with my code ?
EDIT
Based on this thread Kotlin - StateFlow not emitting updates to its collectors, I have changed the declaration of the stateFlow and the way a stateFlow is updated as follow:
object SingletonNav {
// REFACTOR StateFlow
private val _viewState: MutableStateFlow<ScreensState> = MutableStateFlow(ScreensState())
val viewState: StateFlow<ScreensState>
get() = _viewState
fun updateNavState(strg: String) {
// REFACTOR Updating StateFlow
_viewState.value = _viewState.value.copy(paneTwo = strg)
}
fun getPaneTwoState(): String = viewState.value.paneTwo
}
I also changed the way the new state for the second pane is updated:
class PaneTwoViewModel{
// REFACTOR StateFlow
private val _uiState = MutableStateFlow(PaneTwoUiState())
val uiState: StateFlow<PaneTwoUiState>
get() = _uiState
val defaultDispatcher = Dispatchers.Default
val emptyParentJob = Job()
val combinedContext = defaultDispatcher + emptyParentJob
private val scope = CoroutineScope(combinedContext)
init{
scope.launch(context = combinedContext) {
observeNavigationEvent()
}
}
private suspend fun observeNavigationEvent(){
println("PaneTwoViewModel | observeNavigationEvent")
SingletonNav.viewState.collect{
updatePaneTwo(it.paneTwo)
}
}
private fun updatePaneTwo(name: String = ""){
if (name.isNotEmpty()){
// REFACTOR updating StateFlow
_uiState.value = _uiState.value.copy(nameToDisplay = name)
println("PaneTwoViewModel | updatePaneTwo | emitted ${uiState.value.nameToDisplay} ")
}
}
}
The result is the same, the state flow is not not collected. I have no logs for the collecting function and no update of the second pane´s ui.