Hi, I’m kind of new with Appyx library, I was usin...
# appyx
u
Hi, I’m kind of new with Appyx library, I was using the 1.x versions but 2.x has lots of changes. I started the update library and change some nodes but I need help with my issue. I have ‘BottomNavNode’, ‘HomeNode’ and ‘ProfileNode’. Only ‘BottomNavNode’ using spotlight, others are using backstack. My achievement is I want my home and profile node to be live while switching between nodes, but issue is when I switch taps between spotlight items, backstack nodes are rebuild the screens. This cause some issues like, state lose or animation lag. For example switching between screen using SpotlighSlider I can see black background of the node without no content draw. Btw beside this issue, why there is not singleTop for backstack anymore? I can see that on documentation but it is not in the new version of the backstack
z
Hey @Utku Yildiz thanks for the feedback! Any chance you could provide a link to your sample code we could check?
u
I can’t give any link but I can try to make some code-snippets thanks
this is my BottomNavNode
Copy code
class BottomNavNode(
    nodeContext: NodeContext,
    private val serverRequests: ServerRequests,
    private val spotlightModel: SpotlightModel<Routing> = SpotlightModel(
        items = Routing.entries,
        savedStateMap = nodeContext.savedStateMap,
    ),
    private val spotlight: Spotlight<Routing> = Spotlight(
        model = spotlightModel,
        visualisation = {
            SpotlightSlider(it, spotlightModel.currentState)
        },
    ),
) : Node<BottomNavNode.Routing>(
    appyxComponent = spotlight,
    nodeContext = nodeContext,
) {

    enum class Routing {
        Home, Profile
    }

    override fun buildChildNode(
        navTarget: Routing,
        nodeContext: NodeContext
    ): Node<*> = when (navTarget) {
        Routing.Home -> {

            Timber.i("testing nav: Home Created")

            HomeNode(
                nodeContext = nodeContext,
                appStore = appStore,
                serverRequests = serverRequests,
                beatsRepository = beatsRepository,
                navigator = navigator,
                videoRepository = videoRepository,
                analytics = analytics,
            )
        }

        Routing.Profile -> {
            node(nodeContext) {
                Box(modifier = Modifier
                    .fillMaxSize()
                    .clickable { navigationBarNavigate(Routing.Home) }
                    .background(Color.Cyan))
            }
        }
    }
    
    @Composable
    override fun Content(modifier: Modifier) {
        Box(modifier = modifier) {
            AppyxNavigationContainer(appyxComponent = spotlight)
            Box(modifier = Modifier
                .fillMaxWidth()
                .height(100.dp)
                .align(Alignment.BottomCenter)
                .background(Color.Cyan)
                .clickable {
                    navigationBarNavigate(destination = if (spotlight.activeIndex.value == 0f) Routing.Profile else Routing.Home)
                }
            )
        }
    }

    private fun navigationBarNavigate(destination: Routing, pressed: () -> Unit = {}) {
        spotlight.activate(Routing.entries.indexOf(destination).toFloat())
        if (spotlightModel.currentState.activeElement != destination) {
            pressed()
        }
    }
}
This is my HomeNode
Copy code
class HomeNode(
    nodeContext: NodeContext,
    private val videoRepository: VideoRepository,
    private val analytics: Analytics,
    private val serverRequests: ServerRequests,
    private val beatsRepository: BeatsRepository,
    private val appStore: AppStore,
    private val navigator: Navigator,
    private val backStack: BackStack<Routing> = BackStack(
        model = BackStackModel(
            initialTarget = Routing.EventFeed,
            savedStateMap = nodeContext.savedStateMap,
        ),
        visualisation = { BackStackSlider(it) }
    ),
) : Node<HomeNode.Routing>(
    appyxComponent = backStack,
    nodeContext = nodeContext,
) {

    sealed class Routing : Screen {

        @Parcelize
        data object EventFeed : Routing()

    }

    override fun buildChildNode(navTarget: Routing, nodeContext: NodeContext) =
        when (navTarget) {
            is Routing.EventFeed -> node(nodeContext) { nodeModifier ->
                val userDataStorage: UserDataStorage = koinInject()
                HomeController()
            }

        }

    @Composable
    override fun Content(modifier: Modifier) {
        AppyxNavigationContainer(appyxComponent = backStack)
    }

    private fun navigateTo(routing: Routing) {
        backStack.push(routing)
    }

}
it is something like this, sorry I had to remove some part of it
z
No problem removing, but do you think you could move this to a sample repo? It would be really helpful for us if we could launch it and play with it. It doesn’t matter if the UI is broken as long as it demonstrates the issues you flagged.
u
I will need some time for that, sorry. This is not a sample app, and I will need to create one
z
No problem, and thanks your effort.
u
To undestand the consept of the navigation library for myself. am I right about using spotlight with backstack in it. Backstack shouldn’t rebuild the screen right?
z
Yes, it should work just fine with many nested layers, that’s the main idea. That’s why we’d like to take a look at what can cause it to lose state.
u
Good then, I will try to make a sample
@Zsolt This is the sample app that you need. My real structure is same like this. When you select the profile from bottom nav there is a counter, when you increase it and then switch between tabs node will be recreate again and counter will be reset.
z
Hey, thanks for the code, it’s very useful. The broken part is that you have local UI state directly in the composable without making sure it’s persisted. Whenever the Profile screen slides out of bounds it’s removed from the composition. So it’s not that your
backStack
history is not persisted, it’s your UI state. You can do one of the following: 1. Have a dedicated
Node
for the content, not just a composable (you can either extract that part to a new
XYZNode
, or repurpose your
ProfileNode
for this). Then store your counter state in the
Node
, rather than in the composable content. This way it will survive being removed from the composition. Just make sure the state is either a
MutableState
or is guaranteed otherwise to notify the composition of an update (e.g.
Flow
collected as state) 2. Use
rememberSaveable
for state declared in your composable. You can check this sample from the sandbox app in the library, here’s how it persists the random colours associated to back stack elements: https://github.com/bumble-tech/appyx/blob/f5cc89fbdf9a168a70f8e9f11313e6524914179a[…]/appyx/demos/sandbox/navigation/node/backstack/BackStackNode.kt I checked your code with both of the above options. Option 1 works immediately, however for some weird reason Option 2 did not (only for your code – our sample with the colour persistence still works). It’s not obvious at this point why not, so we’ll look into it further. Meanwhile you could go with Option 1 similar to:
Copy code
class XYZNode(
    nodeContext: NodeContext,
) : LeafNode(
    nodeContext = nodeContext
) {
    private var number: Int by mutableIntStateOf(0)

    @Composable
    override fun Content(modifier: Modifier) {
        Column(
            modifier = modifier
                .fillMaxSize()
                .background(Color.Green),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Text(text = "Number $number")
            Button(onClick = { number++ }) {
                Text(text = "Increment")
            }
        }
    }
}
Additionally, a few things not directly related to your issue, just suggestions: 1. You don’t seem to be using the
backStack
instance in your
ProfileNode
at all, so you could have it as a simple
LeafNode
(but maybe this is just because you stripped it down for the demo) 2. If you only have a single target,
Routing.Profile
, it adds some redundancy (your
ProfileNode
already embodies
Profile
, no need for an embedded routing for that) 3. You could potentially benefit from https://bumble-tech.github.io/appyx/navigation/features/material3/
I investigated further and created a ticket for the time being: https://github.com/bumble-tech/appyx/issues/671
u
Hi thanks for answering. As I said before this is just a exmaple of my real app, my real app has a multiple navigation for each node so I can't use leafNode.
Also I tried demo sandbox and I saw the same issues. My colors was not same for each tab switches only the index numbers are still sams because they are inside the navTarget
I tried too many versions of solution to fix it, like material3 way but non of them worked. I can hold my states some different ways but some of my screens has complex logic like video player and when I want to push a new screen like user profile and come back my all video player logic will be recreate. And I believe this is a very big performance issue as well. Also Flickering is very annoying 😄