You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose NavHost页面切换多次重组问题分析与解决

Jetpack Compose NavHost导航时多次重组问题解析

在使用Jetpack Compose的NavHost进行页面导航时,发现即便在基础导航示例中,切换页面也会触发多次重组。以下是最小复现代码:

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Button
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.krokosha.focusrequester2.ui.theme.Test_delete_itTheme

private const val FIRST_SCREEN_ROUTE = "first_screen"
private const val SECOND_SCREEN_ROUTE = "second_screen"

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Test_delete_itTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    shape = RectangleShape
                ) {
                    Greeting()
                }
            }
        }
    }
}

@Composable
fun Greeting() {
    val navigator: NavHostController = rememberNavController()

    NavHost(
        navController = navigator,
        startDestination = FIRST_SCREEN_ROUTE
    ) {
        composable(FIRST_SCREEN_ROUTE) {
            Log.e("HERE", "1 SCREEN")
            FirstScreen(onClick = { navigator.navigate(SECOND_SCREEN_ROUTE) }) }
        composable(SECOND_SCREEN_ROUTE) {
            Log.e("HERE", "2 SCREEN")
            SecondScreen() }
    }
}

@Composable
fun SecondScreen() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Red.copy(alpha = 0.1f)),
        contentAlignment = Alignment.Center
    ) {
        Text(text = "SECOND SCREEN")
    }
}

@Composable
fun FirstScreen(onClick: () -> Unit) {
    Row(modifier = Modifier
        .fillMaxSize()
    ) {
        LeftPanel()
        RightPanel(onClick = onClick)
    }
}

@Composable
fun RowScope.LeftPanel() {
    val buttons: List<String> by rememberSaveable { mutableStateOf(List(5) { "Button ${it + 1}" }) }

    LazyColumn(
        modifier = Modifier
            .background(Color.Blue.copy(alpha = 0.1f))
            .fillMaxHeight()
            .weight(1f),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        itemsIndexed(
            items = buttons,
            key = { idx, _ -> idx }
        ) { idx, _ ->
            Button(
                modifier = Modifier,
                onClick = { /* nothing */ }
            ) {
                Text(text = "Left Panel: $idx")
            }
        }
    }
}

@Composable
fun RowScope.RightPanel(onClick: () -> Unit) {
    val buttons: List<String> by rememberSaveable { mutableStateOf(List(4) { "Button ${it + 1}" }) }

    Column(
        modifier = Modifier
            .background(Color.Green.copy(alpha = 0.1f))
            .fillMaxHeight()
            .weight(1f),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        LazyVerticalGrid(
            modifier = Modifier.padding(16.dp),
            columns = GridCells.Fixed(2),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            itemsIndexed(
                items = buttons,
                key = { idx, _ -> idx }
            ) { idx, _ ->
                Button(
                    modifier = Modifier
                        .padding(8.dp),
                    onClick = {
                        onClick()
                    }
                ) {
                    Text(text = "Right Panel: $idx")
                }
            }
        }
    }
}

点击右侧面板按钮后,日志输出如下:

13:26:43.572  E  1 SCREEN
13:26:46.570  E  1 SCREEN
13:26:46.580  E  2 SCREEN
13:26:46.661  E  1 SCREEN
13:26:46.668  E  2 SCREEN
13:26:47.369  E  2 SCREEN
13:26:47.438  E  2 SCREEN

问题原因与修复

初步排查:日志位置干扰

最初日志放在composable代码块内,而Compose重组是从顶层父元素开始遍历检查参数变化,Greeting函数重组时会触发这些日志,导致误判。将日志移至FirstScreen和SecondScreen内部后,确认这两个页面确实被多次重绘。

核心问题:不稳定的Lambda参数

问题根源在于FirstScreen(onClick = { navigator.navigate(SECOND_SCREEN_ROUTE) })这行代码:每次重组时都会创建一个新的Lambda实例,导致FirstScreen的参数不稳定,触发不必要的重组。

修复方案

使用remember保存Lambda,确保其实例稳定:

@Composable
fun Greeting() {
    val navigator: NavHostController = rememberNavController()

    val click = remember { { navigator.navigate(SECOND_SCREEN_ROUTE) } }

    NavHost(
        navController = navigator,
        startDestination = FIRST_SCREEN_ROUTE
    ) {
        composable(FIRST_SCREEN_ROUTE) {
            FirstScreen(onClick = { click() })
        }
        composable(SECOND_SCREEN_ROUTE) {
            SecondScreen()
        }
    }
}

内容的提问来源于stack exchange,提问作者Sirop4ik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 00:27:09