0

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.

eqtèöck
  • 971
  • 1
  • 13
  • 27
  • I reopened, because after the edit it no longer seems to be a problem with mutating the data stored in a flow. – broot Aug 23 '23 at 06:36
  • Please recheck everything. I believe the above code should work at least to the point of "PaneTwoViewModel | updatePaneTwo" log. If it doesn't then maybe you don't use exactly the code you posted? I removed irrelevant code and it runs fine: https://pl.kotl.in/9TPeIDbT8 . I see the log from `updatePaneTwo`. – broot Aug 23 '23 at 06:57
  • I'm not familiar with Compose, but your `App` function feels strange to me. If it is a composable function, then I think it shouldn't create its own view model. This function is called whenever the UI has to recompose, so it would create multiple view models. – broot Aug 23 '23 at 07:03

0 Answers0