Navigate to another screen without the previous scaffold in Jetpack Compose
Asked Answered
G

2

5

My app has a main screen with a Scaffold and a BottomNavigation bar:

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController = rememberNavController()
            MyApplicationTheme {
                Scaffold(
                    bottomBar = {
                        BottomBar(navController = navController)
                    }
                ) {
                    NavigationGraph(navController = navController)
                }
            }
        }
    }

...

@Composable
fun NavigationGraph(navController: NavHostController){
    NavHost(navController = navController, startDestination = BottomMenuOption.Home.route) {

        composable(route = BottomMenuOption.Home.route) {
            HomeScreen(navController = navController)
        }

        composable(route = BottomMenuOption.Settings.settings) {
            SettingsScreen()
        }
        composable(route = BottomMenuOption.Profile.route) {
            ProfileScreen()
        }

       composable(route = "feature") {
           FeatureScreen()
       }
    }

}

FeatureScreen has it's own Scaffold with a topBar instead of a bottomBar and when I navigate to it from HomeScreen, I want to replace the previous one from the main screen and just see a topBar but instead, I'm seeing the two bars in the screen.

@Composable
fun FeatureScreen() {
    Scaffold (
        topBar = {
            TopBar(" Feature Screen")
        }
            )  {

    }
}

Is it possible to accomplish this? I'm thinking it could be done by just using a new Activity but ideally, I would like to keep the Single Activity approach.

Giesecke answered 2/1, 2022 at 23:46 Comment(0)
T
3

I would suggest creating a new function like this:

@Composable
fun MainScaffold(
    topBar: @Composable (() -> Unit) = {},
    bottomBar: @Composable (() -> Unit) = {},
    content: @Composable (PaddingValues) -> Unit){
    Scaffold(
        bottomBar = bottomBar,
        topBar = topBar,
        content = content
    )
}

then, use this main scaffold in your screens:

@Composable
fun HomeScreen(navController: NavHostController) {
    MainScaffold(
        bottomBar = { BottomBar(navController = navController) },
        content = {
        // content
     })
}

and in your feature screen:

@Composable
fun FeatureScreen() {
    MainScaffold (
        topBar = {
            TopBar(" Feature Screen")
        }
    )  {
        //content
    }
}

and in setContent

setContent {
       val navController = rememberNavController()
       VoiceAssistantJetpackComposeTheme {
           NavigationGraph(navController = navController)
       }}
Transgress answered 3/1, 2022 at 11:58 Comment(2)
Yeah, this did the trick nicely. Thanks!Giesecke
With this solution BottomBar is not shared between the screens, is that the desired behavior? Won't UI flick when switching between tabs since the bottom bar has to be drawn again and again?Probst
A
3

I don’t know how long it will take you to see this answer. Maybe two years later, maybe five years later. No matter how long it takes, I'm glad I could help you.

This is an answer reserved for latecomers.

The only thing you have to remember is that callbacks are the soul of this solution.

Come, let's take a look at my simple app, it's called Kite. It has a HomeScreen with three screens, this means the three screens share the same scaffold. On the left is a ChatListScreen. Click on the list item to jump to the ChatScreen.

The problem we want to solve is: when jumping from the ChatListScreen to the ChatScreen, can we not bring the scaffold of the home page?

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)

        setContent {
            KiteApp()
        }
    }
}
@Composable
fun KiteApp() {
    KiteTheme {
        val navController = rememberNavController() // 确保在调用 NavHost 之前初始化
        NavHost(
            navController = navController,
            // wow, look, it means HomeScreen is the default screen to show
            startDestination = "HomeScreen",
            modifier = Modifier
        ) {
            composable("HomeScreen") {
                HomeScreen(
                    // Note here that the three screens in HomeScreen 
                    // are all provided through lists.
                    screens = listOf(
                        Screen(
                            label = "Chat",
                            icon = Icons.Default.Face,
                            content = {
                                // Note here that the action from ChatListScreen to ChatScreen 
                                // is provided through the click callback.
                                ChatListScreen { chatGroup ->
                                    // use the route to open ChatScreen
                                    navController.navigate("Chat/${chatGroup.id}")
                                }
                            }
                        ),
                        Screen(
                            label = "Discover",
                            icon = Icons.Default.Home,
                            content = { DiscoverScreen() }
                        ),
                        Screen(
                            label = "Profile",
                            icon = Icons.Default.Person,
                            content = { ProfileScreen() }
                        )
                    )
                )
            }
            // register the route to open ChatScreen
            composable("Chat/{chatId}") { backStackEntry ->
                val chatId = backStackEntry.arguments?.getString("chatId") ?: ""
                ChatScreen(chatId)
            }
        }
    }
}

Remember, try to add click event callback parameters to the Composable function instead of navController parameter.

If you pass navController parameters to Composable functions, this will not only make your routing logic scattered and code coupled, but sometimes you will also encounter strange exceptions.

Look at my HomeScreen implementation.

data class Screen(
    val label: String,
    val icon: ImageVector,
    val content: @Composable () -> Unit
)

@Composable
fun HomeScreen(
    screens: List<Screen>
) {
    var selectedItem by remember { mutableStateOf(0) }

    Scaffold(
        bottomBar = {
            BottomNavigation {
                screens.forEachIndexed { index, screen ->
                    BottomNavigationItem(
                        icon = { Icon(screen.icon, contentDescription = null) },
                        label = { Text(screen.label) },
                        selected = selectedItem == index,
                        onClick = { selectedItem = index }
                    )
                }
            }
        }
    ) { innerPadding ->
        Box(modifier = Modifier.padding(innerPadding)) {
            screens[selectedItem].content()
        }
    }
}

In fact, this is enough, but for the completeness of this example, I'm going to show you how to implement ChatListScreen.

@Composable
fun ChatListScreen(onChatGroupClick: (ChatGroup) -> Unit) {
    val chatGroups = listOf(
        ChatGroup("1", "Group 1", R.drawable.avatar1, "Hello! How are you?"),
        ChatGroup("2", "Group 2", R.drawable.avatar2, "Let's meet tomorrow."),
        // Add more chat groups here
    )

    LazyColumn {
        items(chatGroups) { chatGroup ->
            ChatListItem(chatGroup = chatGroup, onClick = { onChatGroupClick(chatGroup) })
            Divider()
        }
    }
}

Finally, I'll show you the code for ChatScreen function that has its own scaffold.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChatScreen(chatId: String) {
    var currentRole by remember { mutableStateOf(Roles.roles[0]) }
    val messages = remember { mutableStateListOf<Message>() }
    // Fetch chat group based on chatId (this is a placeholder)
    val chatGroup = remember { "myself" }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Chat with ${chatGroup}") },
                actions = {
                    IconButton(onClick = {
                        currentRole =
                            if (currentRole.id == Roles.roles[0].id) Roles.roles[1] else Roles.roles[0]
                    }) {
                        Avatar(
                            role = currentRole,
                            avatarSize = 40.dp,
                            onAvatarDoubleClick = { role ->
                                currentRole =
                                    if (role.id == Roles.roles[0].id) Roles.roles[1] else Roles.roles[0]
                            }
                        )
                    }
                }
            )
        }
    ) { innerPadding ->
        Box(
            Modifier
                .padding(innerPadding)
                .navigationBarsPadding()
                .imePadding()
        ) {
            ChatContent(
                messages = messages,
                currentRole = currentRole,
                onSendMessage = { message ->
                    messages.add(Message(message, currentRole))
                },
                onAvatarDoubleClick = { role ->
                    if (role.id != currentRole.id) {
                        currentRole = role
                    }
                }
            )
        }
    }
}

@Composable
fun ChatContent(
    messages: List<Message>,
    currentRole: Role,
    onSendMessage: (String) -> Unit,
    onAvatarDoubleClick: (Role) -> Unit,
    modifier: Modifier = Modifier
) {
    var message by remember { mutableStateOf("") }

    Column(
        modifier = modifier
    ) {
        LazyColumn(
            modifier = Modifier
                .weight(1f)
                .fillMaxWidth(),
            reverseLayout = true
        ) {
            items(messages.reversed()) { msg ->
                ChatMessage(
                    message = msg,
                    currentRole = currentRole,
                    onAvatarDoubleClick = onAvatarDoubleClick
                )
            }
        }

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp)
                .imePadding(),
            verticalAlignment = Alignment.Bottom
        ) {
            ChatInputField(
                value = message,
                onValueChange = { message = it },
                modifier = Modifier.weight(1f)
            )
            Button(
                onClick = {
                    if (message.isNotBlank()) {
                        onSendMessage(message)
                        message = ""
                    }
                },
                modifier = Modifier.padding(start = 8.dp)
            ) {
                Text("Send")
            }
        }
    }
}

You don't need to care about how ChatScreen is implemented.

You just need to remember that when jumping from ChatListScreen to ChatScreen, it will not nest the Scaffold of HomeScreen outside ChatScreen.

I think you can easily understand why this is the case.

Over.

Avocado answered 9/6 at 10:18 Comment(1)
I appreciate the thoughtful response after all of this time. I actually reached a solution not dissimilar to this one.Giesecke

© 2022 - 2024 — McMap. All rights reserved.