我尝试遵循 android 文档中的示例,但无法使用 Navigation Compose、ViewModel 和 Hilt 实现导航。不会引发任何错误,并且在 NavigationScreen LaunchedERffect 中不会在 ViewModel 发出时触发。
可组合项:
@HiltAndroidApp
class MyApplication : Application() {}
@Serializable
object NavScreen
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val navController = rememberNavController()
PhotosSyncTheme {
NavHost(
navController = navController,
startDestination = NavScreen
) {
composable<NavScreen> { NavigationScreen(navController) }
composable<NavigationEvent.NavigateToHome> { HomeScreen() }
composable<NavigationEvent.NavigateToSettings> { Settings() }
}
}
}
}
}
@Composable
fun NavigationScreen(navController: NavController) {
val navViewModel = hiltViewModel<NavigationViewModel>()
val navigationEvent by navViewModel.navigationEvents.collectAsState(initial = NavigationEvent.NavigateToHome)
LaunchedEffect(navigationEvent) {
Log.d("Navigation Screen", navigationEvent.toString())
navigationEvent.let { event ->
Log.d("Navigation Screen", event.toString())
when (event) {
is NavigationEvent.NavigateBack -> navController.popBackStack()
NavigationEvent.NavigateToHome -> navController.navigate(NavigationEvent.NavigateToHome)
NavigationEvent.NavigateToSettings -> navController.navigate(NavigationEvent.NavigateToSettings)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen() {
val navViewModel = hiltViewModel<NavigationViewModel>()
var selectedItem by remember { mutableIntStateOf(0) }
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(title = { Text("Photos Sync") }, actions = {
IconButton(onClick = {
navViewModel.navigateToSettings()
}) {
Icon(
imageVector = Icons.Filled.Settings,
contentDescription = "Settings icon"
)
}
})
}
)
{ }
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Settings() {
val viewModel = hiltViewModel<NavigationViewModel>()
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBarWithBack(title = "Definições")
}
)
{ }
}
视图模型:
@HiltViewModel
class NavigationViewModel @Inject constructor() : ViewModel() {
private val _navigationEvents = MutableSharedFlow<NavigationEvent>()
val navigationEvents: SharedFlow<NavigationEvent> = _navigationEvents.asSharedFlow()
fun navigateBack() {
viewModelScope.launch { _navigationEvents.emit(NavigationEvent.NavigateBack) }
}
fun navigateToHome() {
viewModelScope.launch { _navigationEvents.emit(NavigationEvent.NavigateToHome) }
}
fun navigateToSettings() {
viewModelScope.launch {
Log.d("Navigation ViewModel", "Emit Navigate To Settings")
try {
_navigationEvents.emit(NavigationEvent.NavigateToSettings)
} catch (e: Exception) {
Log.e("Navigation ViewModel", "Error emitting navigation event")
} finally {
Log.d("Navigation ViewModel", "done Emit Navigate To Settings")
}
}
}
fun navigateTo(event: NavigationEvent) {
viewModelScope.launch { _navigationEvents.emit(event) }
}
sealed class NavigationEvent {
@Serializable
object NavigateBack : NavigationEvent()
@Serializable
object NavigateToHome : NavigationEvent()
@Serializable
object NavigateToSettings : NavigationEvent()
}
}
我期望当我单击设置按钮时,使用
navController.navigate
将设置可组合项推送到可组合项堆栈的顶部
视图模型的范围仅限于当前导航目的地(无论您是否使用 Hilt)。您对不同的屏幕使用相同的视图模型,因此每个屏幕都会获得其“自己的”视图模型实例。修改其中一个的状态不会影响其他的状态。 您可以将视图模型范围限制为其他内容,以便您的目的地将共享
相同视图模型实例(请参阅我可以在不同的 Compose 导航路线中使用 hiltViewModel() 共享 ViewModel 吗?),但我不明白为什么您甚至想首先涉及视图模型,因为导航与 UI 状态无关。您的视图模型当前所做的就是接收导航事件并将其传递回可组合项。您甚至不需要保留当前路线或其参数(可以使用SavedStateHandle
来完成),但这对于专用于特定屏幕的视图模型也最有用,而不是导航视图模型。 我建议完全删除
NavigationViewModel
。您可以保留您的
NavigationEvent
密封层次结构,尽管我发现这个名称很有误导性,而且您甚至不需要继承。最好用这样的东西替换它(在顶层声明,没有封闭的接口):@Serializable data object HomeRoute
@Serializable data object SettingsRoute
由于您不再将它们用作事件,因此它们现在仅用于导航图中的路线,因此得名。由于
NavigateBack
不是一条路线,因此可以将其删除(只需在需要时调用
navController.popBackStack()
)。您的 NavHost 将如下所示:
NavHost(
navController = navController,
startDestination = HomeRoute,
) {
composable<HomeRoute> {
HomeScreen(
navigateToSettings = { navController.navigate(SettingsRoute) },
)
}
composable<SettingsRoute> { Settings() }
}
请注意,我创建了新参数
HomeScreen
来传递必要的导航逻辑,而不是
navigateToSettings
依赖视图模型来访问导航。你不应该传递 navController 。 HomeScreen
然后使用新参数,如下所示:@Composable
fun HomeScreen(
navigateToSettings: () -> Unit,
) {
// ...
IconButton(onClick = navigateToSettings) {
//...
}
}
这也使得 HomeScreen 具有更好的可测试性和可重用性,因为它不再依赖于视图模型。
整个导航逻辑现在包含在 NavHost 中。
NavigationScreen
带有 LaunchedEffect 的可以删除。
Google 提供了功能齐全的示例项目 Now in Android,除其他外,还展示了如何使用更复杂的导航。如果您想窥探,只需使用以下链接将其导入 Android Studio(“从版本控制获取...”):https://github.com/android/nowinandroid