android 在Jetpack Compose + Hilt + ViewModel中从ViewModel导航的正确方法是什么?

neekobn8  于 2023-05-05  发布在  Android
关注(0)|答案(3)|浏览(378)

我偶然发现了这个很琐碎但很棘手的问题。我已经花了相当多的时间搜索官方文档,但不幸的是没有找到答案。
官方文档说你应该将NavController的示例传递给@Composable-s,并将其称为onClick = { navController.navigate("path") }。但是如果我必须从ViewModel触发导航事件(例如:登录时重定向,重定向到新创建的帖子页面)?等待任何协程(例如HTTP请求)在@Composable中不仅是糟糕的,而且可能会迫使Android因为UI线程被阻塞而杀死应用程序
非官方的解决方案(大多以Medium文章的形式记录)是基于拥有一个单例类并观察一些MutableStateFlow包含路径的概念。
这在理论上听起来很愚蠢,在实践中也没有多大帮助(没有副作用和重组友好,触发不必要的重新导航)。

z4bn682m

z4bn682m1#

我自己也一直在纠结同样的问题。从Google提供的关于这个主题的有限文档中,特别是架构事件部分,我想知道他们建议的是使用状态作为导航的触发器吗?
引用文件:
例如,在实现登录屏幕时,轻按登录按钮应会导致您的应用显示进度微调器和网络呼叫。如果登录成功,则您的应用将导航到另一个屏幕;在错误的情况下,应用程序显示Snackbar。下面是如何对屏幕状态和事件进行建模:
他们为上述要求提供了以下代码片段:

sealed class UiState {
    object SignedOut : UiState()
    object InProgress : UiState()
    object Error : UiState()
    object SignIn : UiState()
}

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(SignedOut)
    val uiState: State<UiState>
        get() = _uiState
}

他们没有提供的是视图模型的其余部分和编写代码。我猜应该是这样的

@Composable
fun MyScreen(navController: NavController, viewModel: MyViewModel) {
    when(viewModel.uiState){
        is SignedOut ->  // Display signed out UI components
        is InProgress -> // Display loading spinner
        is Error ->      // Display error toast

        // Using the SignIn state as a trigger to navigate
        is SignIn ->     navController.navigate(...)  
    }
}

视图模型也可以有一个类似这样的函数(通过单击撰写屏幕上的“登录”按钮来触发

fun onSignIn() {
    viewModelScope.launch {
        // Make a suspending sign in network  call
        _uiState.value = InProgress

         // Trigger navigation
        _uiState.value = SignIn
    }
}
z2acfund

z2acfund2#

rememberNavController有一个非常简单的源代码,您可以使用它在单例服务中创建它:

@Singleton
class NavigationService @Inject constructor(
    @ApplicationContext context: Context,
) {
    val navController = NavHostController(context).apply {
        navigatorProvider.addNavigator(ComposeNavigator())
        navigatorProvider.addNavigator(DialogNavigator())
    }
}

创建一个helper视图模型,以将NavHostControllerNavHost视图共享:

@HiltViewModel
class NavViewModel @Inject constructor(
    navigationService: NavigationService,
): ViewModel() {
    val controller = navigationService.navController
}

NavHost(
    navController = hiltViewModel<NavViewModel>().controller,
    startDestination = // ...
) {
    // ...
}

然后,在任何视图模型中,您都可以注入它并用于导航:

@HiltViewModel
class ScreenViewModel @Inject constructor(
    private val navigationService: NavigationService
): ViewModel() {
    fun navigateToNextScreen() {
        navigationService.navController.navigate(Destinations.NextScreen)
    }
}
u5rb5r59

u5rb5r593#

我走了一条类似于@Phil Dukhov的路。我创建了一个 Package 器类,它复制了已经在rememberNavController()中找到的代码:

class NavigationService constructor(
    context: Context,
) {
    val navController = NavHostController(context).apply {
        navigatorProvider.addNavigator(ComposeNavigator())
        navigatorProvider.addNavigator(DialogNavigator())
    }
}

然后使用Hilt为我的NavHostController创建了一个提供程序。因为我需要navController遍历嵌套的NavHost,所以我决定将其范围限定为ViewModel

@Module
@InstallIn(ViewModelComponent::class)
object NavigationModule {
    @Provides
    fun provideNestedNavController(@ApplicationContext context: Context): NavHostController {
        return NavigationService(context).navController}
}

这允许我将navcontroller直接注入到视图模型中,并从内部触发导航。然后,我可以通过以下方式从我的合成对象访问NavController:

val navController: NavHostController = viewModel.navController

为了构建嵌套的NavGraph

相关问题