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.