adrielcafe / voyager

🛸 A pragmatic navigation library for Jetpack Compose
https://voyager.adriel.cafe
MIT License
2.31k stars 109 forks source link

Navigation Between Screens Causing Recomposition That Shouldn't Happen In Compose Multiplatform Mobile #307

Closed Mukuljangir372 closed 4 months ago

Mukuljangir372 commented 4 months ago

In Compose Multiplatform mobile (Android & iOS), Currently, Navigation between screens causing unnecessary recomposition and loading the entire voyager screen after coming back to same screen . Is there any way to avoid the recomposition during navigation? We're attaching codebase for better understanding. If you paid attention to the code, If I switch between tickets or teams tab from Dashboard screen, It is causing the recomposition and as the result, the chat list is recomposition too. Can we avoid reloading or recomposition of screens? Thanks.

Steps To Reproduce

  1. Setup Navigator (Splash screen is initial screen)

    @Composable
    fun App() {
    ScogoThemeUi {
        Navigator(AppNavigationImpl().initialScreen())
    }
    }
  2. Dashboard Screen (Splash screen navigates to it)

    
    internal class DashboardScreen : Screen, KoinComponent {
    @Composable
    override fun Content() {
        DashboardUiScreen()
    }
    }

@Composable private fun DashboardUiScreen() { val stateHolder by remember { mutableStateOf(DashboardStateHolder()) } val bottomNavItems = stateHolder.getBottomNavItems()

TabNavigator(TicketsTab()) {
    val selectedScreen by stateHolder.selectedScreen.collectAsState()
    Scaffold(
        modifier = Modifier.fillMaxSize(),
        bottomBar = {
            NavigationBar(modifier = Modifier.fillMaxWidth()) {
                bottomNavItems.forEach { item ->
                    val selected = selectedScreen == item.screen
                    NavigationBarItem(
                        selected = selected,
                        icon = {
                            Icon(
                                painter = rememberVectorPainter(item.icon),
                                contentDescription = null,
                                tint = if (selected) {
                                    ScogoTheme.colorScheme.onSecondary
                                } else {
                                    ScogoTheme.colorScheme.outlineVariant
                                }
                            )
                        },
                        label = {
                            Text(
                                text = item.label,
                                style = ScogoTheme.typo.bodySmall,
                                color = if (selected) {
                                    ScogoTheme.colorScheme.secondary
                                } else {
                                    ScogoTheme.colorScheme.outlineVariant
                                }
                            )
                        },
                        onClick = {
                            stateHolder.selectedScreen(item.screen)
                        }
                    )
                }
            }
        },
        content = {
            CurrentTab()
        }
    )

    val tabNavigator = LocalTabNavigator.current
    when (selectedScreen) {
        DashboardNavScreen.Tickets -> {
            tabNavigator.current = TicketsTab()
        }

        DashboardNavScreen.Teams -> {
            tabNavigator.current = TeamsTab()
        }
    }
}

}

private class TicketsTab : Tab, KoinComponent { override val options: TabOptions @Composable get() = remember { TabOptions(index = 0u, title = "") }

@Composable
override fun Content() {
    val navigation: AppNavigation by inject()
    navigation.toTicketContainer(null).Content()
}

}

private class TeamsTab : Tab { override val options: TabOptions @Composable get() = remember { TabOptions(index = 0u, title = "") }

@Composable
override fun Content() {
    // TODO: Screen
    Text("Teams")
}

}


3. Ticket Container Screen (TicketsTab)
```kotlin
internal class TicketContainerScreen : Screen, KoinComponent {
    @Composable
    override fun Content() {
        val navigation: AppNavigation by inject()
        TicketContainerUiScreen(navigation = navigation)
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun TicketContainerUiScreen(navigation: AppNavigation) {
    val stateHolder by remember { mutableStateOf(TicketContainerStateHolder()) }

    val tabs by remember(stateHolder) { mutableStateOf(stateHolder.getTabs()) }
    val selectedTab by stateHolder.selectedTab.collectAsState()

    val pagerState = rememberPagerState { tabs.size }

    LaunchedEffect(selectedTab) {
        pagerState.animateScrollToPage(selectedTab.index)
    }

    LaunchedEffect(pagerState) {
        snapshotFlow { pagerState.currentPage }.collectLatest { page ->
            stateHolder.selectedTab(stateHolder.getTabByIndex(page))
        }
    }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        topBar = {
            Column(modifier = Modifier.fillMaxWidth()) {
                TopAppBars.TextTitle(
                    modifier = Modifier.fillMaxWidth(),
                    title = ScogoTheme.strings.dashboard
                )

                TabRow(
                    modifier = Modifier.fillMaxWidth(),
                    containerColor = ScogoTheme.colorScheme.onPrimary,
                    selectedTabIndex = selectedTab.index
                ) {
                    tabs.forEach { tab ->
                        Tab(
                            selected = tab == selectedTab,
                            onClick = {
                                stateHolder.selectedTab(tab)
                            },
                            text = {
                                Text(
                                    fontWeight = FontWeight.Bold,
                                    text = tab.label
                                )
                            }
                        )
                    }
                }
            }
        }
    ) { paddingValues ->
        HorizontalPager(
            modifier = Modifier.fillMaxSize().padding(paddingValues),
            state = pagerState
        ) { index ->
            when (stateHolder.getTabByIndex(index)) {
                TicketContainerTab.Chats -> {
                    navigation.toChatList(null).Content()
                }

                TicketContainerTab.NewTickets -> {
                    navigation.toNewTicketList(null).Content()
                }

                TicketContainerTab.Tickets -> {
                    navigation.toTicketList(null).Content()
                }
            }
        }
    }
}
  1. Chat List Screen (TicketContainerTab.Chats)
    
    class ChatListScreen : Screen, KoinComponent {
    @Composable
    override fun Content() {
        val viewModel: ChatListViewModel by inject()
        val navigation: AppNavigation by inject()
        ChatListUiScreen(
            viewModel = viewModel,
            onChatClick = { chat ->
                navigation.toChatMessageList(
                    navigator = navigation.rootNavigator,
                    conversationId = chat.id
                )
            }
        )
    }
    }

@Composable private fun ChatListUiScreen( viewModel: ChatListViewModel, onChatClick: (ChatDisplayModel) -> Unit ) { val state by viewModel.uiState.collectAsState(ChatListUiState.Idle)

LaunchedEffect(Unit) {
    viewModel.start()
}

Scaffold(
    modifier = Modifier
        .fillMaxSize()
        .padding(bottom = ScogoTheme.spacing.ten.dp)
) {
    when (state) {
        is ChatListUiState.Loading -> {
            Shimmer(modifier = Modifier.fillMaxSize())
        }

        is ChatListUiState.ChatList -> {
            ChatList(
                state = state as ChatListUiState.ChatList,
                onScrolledToBottom = viewModel::onChatListEnded,
                onRefresh = viewModel::onRefresh,
                onChatClick = onChatClick
            )
        }

        is ChatListUiState.NotResults -> {
            NotChatResults(modifier = Modifier.fillMaxSize())
        }

        else -> {}
    }
}

}


5. AppNavigation
```kotlin
internal class AppNavigationImpl : AppNavigation {
    private var _rootNavigator: Navigator? = null
    override val rootNavigator: Navigator? get() = _rootNavigator

    override fun setRootNavigator(navigator: Navigator) {
        _rootNavigator = navigator
    }

    override fun initialScreen(): Screen {
        return SplashScreen()
    }

    override fun toDashboard(navigator: Navigator?): Screen {
        val screen = DashboardScreen()
        navigator?.push(screen)
        return screen
    }

    override fun toTicketContainer(navigator: Navigator?): Screen {
        val screen = TicketContainerScreen()
        navigator?.push(screen)
        return screen
    }

    override fun toChatList(navigator: Navigator?): Screen {
        val screen = ChatListScreen()
        navigator?.push(screen)
        return screen
    }
}

Attachments

Screenshot 2024-01-16 at 12 18 25 PM

Versions Information

Kotlin: 1.9.20
Voyager: 1.0.0-rc05
Syer10 commented 4 months ago

Thats a pretty old version of Voyager, have you not tested on a new one?

Mukuljangir372 commented 4 months ago

@Syer10 Issue is same with latest version.