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

发布于 2025-01-24 07:28:33 字数 487 浏览 0 评论 0原文

我偶然发现了这个非常琐碎但棘手的问题。我花了很多时间搜索官方文档,但不幸的是没有找到答案。

官方文档说,您应该将navcontroller的实例传递给@composable -s,并将其称为onclick = {navcontroller.navigate(“ path”) }。但是,如果我必须从ViewModel触发导航事件(例如,重定向到登录,重定向到新创建的Post Page),会发生什么?等待@composable中的任何coroutine(ex。http请求)不仅很糟糕,而且可能会迫使Android杀死App,因为被阻止的UI线程

非正式的解决方案(主要记录了中等文章的形式)是基于拥有单身类和观察一些mutableStateFlow包含路径的概念。

从理论上讲,这听起来很愚蠢,并且在实践中没有太大帮助(副作用和重新组成友好,触发不必要的重新启动)。

I have stumbled upon this quite trivial, but tricky problem. I have spent a decent amount of time searching official docs, but unfortunately found no answer.

Official docs say that you should pass an instance of NavController down to @Composable-s, and call it as onClick = { navController.navigate("path") }. But what happens if I have to trigger navigation event from ViewModel (ex. redirect on login, redirect to newly created post page)? Awaiting any coroutine (ex. HTTP request) in @Composable would be not just bad, but probably force Android to kill app because of the blocked UI thread

Unofficial solutions (documented mostly if form of Medium articles) are based on the concept of having a singleton class and observing some MutableStateFlow containing path.

That sounds stupid in theory, and doesn't help much in practice (not side-effect and recomposition friendly, triggers unnecessary re-navigation).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

↙温凉少女 2025-01-31 07:28:33

我自己一直在挣扎着完全相同的问题。 特别是体系结构事件部分 i i'我想知道他们的建议是将状态用作导航的触发因素吗?

引用文档:

例如,在实现登录屏幕时,点击登录按钮应导致您的应用显示进度旋转器和网络调用。如果登录成功,则您的应用程序会导航到其他屏幕;如果出现错误,该应用显示一个Snackbar。您将如何建模屏幕状态和事件:

They have provided the following snippet of code for the above requirement:

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
}

What they did not provide is the rest of the view model and compose code. I'm guessing it's supposed to look like:

@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(...)  
    }
}

Also the view model could have a function like this one (trigger by clicking a "sign in" button from compose screen

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

         // Trigger navigation
        _uiState.value = SignIn
    }
}

I have been struggling with the exact same question myself. From the limited documentation Google provided on this topic, specifically the architecture events section I'm wondering if what they're suggesting is to use a state as a trigger for navigation?

Quoting the document:

For example, when implementing a sign-in screen, tapping on a Sign in button should cause your app to display a progress spinner and a network call. If the login was successful, then your app navigates to a different screen; in case of an error the app shows a Snackbar. Here's how you would model the screen state and the event:

They have provided the following snippet of code for the above requirement:

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
}

What they did not provide is the rest of the view model and compose code. I'm guessing it's supposed to look like:

@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(...)  
    }
}

Also the view model could have a function like this one (trigger by clicking a "sign in" button from compose screen

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

         // Trigger navigation
        _uiState.value = SignIn
    }
}
小矜持 2025-01-31 07:28:33

continue

The rememberNavController has a pretty simple source code that you can use to create it in a singleton service:

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

Create a helper view model to share NavHostController with NavHost view:

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

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

Then in any view model you can inject it and use for navigation:

@HiltViewModel
class ScreenViewModel @Inject constructor(
    private val navigationService: NavigationService
): ViewModel() {
    fun navigateToNextScreen() {
        navigationService.navController.navigate(Destinations.NextScreen)
    }
}
若有似无的小暗淡 2025-01-31 07:28:33

我的方式比@phil Dukhov类似。我创建了一个包装程序类,该类复制已经在remame> remamenavcontroller()中找到的代码:

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

然后使用刀柄我为我的navHostController创建了一个提供商。由于我需要navController遍历嵌套navHost,因此决定将其范围范围范围范围固定到viewModel

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

这使我可以直接将navController注入我的ViewModel,并从内部触发导航。然后,我可以通过以下方式从我的组件访问NavController:

val navController: NavHostController = viewModel.navController  

为了构建嵌套navgraph

I went a similar way than @Phil Dukhov. I created a wrapper class that copies the code already found in rememberNavController():

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

Then using Hilt I created a provider for my NavHostController. As I needed my navController to traverse a nested NavHost I decided to scope it to the ViewModel

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

This allows me to inject the navcontroller directly into my viewmodel and trigger navigation from within. I can then access the NavController from my composables in the following way:

val navController: NavHostController = viewModel.navController  

in order to build the nested NavGraph

无边思念无边月 2025-01-31 07:28:33

我已经使用了几个月,但没有遇到任何问题。我喜欢它,因为它很简单。我的产品版本使用Koin注入NavigationManager Singleton(而不是Hilt)。为了简化此示例,我发布了一种基于对象的方法。

sealed interface Route {
    @Serializable
    data object Home : Route

    @Serializable
    data object Screen1 : Route
}

/**
 * Centralized navigation manager that emits screen events
 * to be consumed by the [NavigatorLaunchedEffect] composable.
 */
object NavigationManager {
    private val _route = MutableSharedFlow<Route>()
    val route: SharedFlow<Route> = _route

    fun navigateTo(event: Route) {
        _route.tryEmit(event)
    }

    suspend fun navigateToAsync(event: Route) {
        _route.emit(event)
    }
}

/**
 * This composable LaunchedEffect collects navigation events
 * from the [NavigationManager] and navigates to the next
 * emitted screen.
 */
@Composable
fun NavigatorLaunchedEffect(
    navController: NavHostController,
) {
    LaunchedEffect("NavigationEvents") {
        NavigationManager.route.collect { screen ->
            navController.navigate(screen)
        }
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface {
                    val navController = rememberNavController()

                    // Listen and respond to navigation events.
                    NavigatorLaunchedEffect(navController = navController)

                    NavHost(navController = navController, startDestination = Route.Home) {
                        composable<Route.Home> {  
                            HomeScreen()
                        }
                        composable<Route.Screen1> {
                            Screen1()
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun HomeScreen() {
    Button(
        onClick = {
            NavigationManager.navigateTo(Route.Screen1)
        }
    ) {
        Text("Navigate to Screen1")
    }
}

@Composable
fun Screen1() {
    Text("Screen1")
}

I've used this for a few months and haven't encountered any issues. I like it because it's straightforward. My product version uses Koin to inject a NavigationManager singleton (instead of Hilt). To simplify this example, I posted an object-based approach.

sealed interface Route {
    @Serializable
    data object Home : Route

    @Serializable
    data object Screen1 : Route
}

/**
 * Centralized navigation manager that emits screen events
 * to be consumed by the [NavigatorLaunchedEffect] composable.
 */
object NavigationManager {
    private val _route = MutableSharedFlow<Route>()
    val route: SharedFlow<Route> = _route

    fun navigateTo(event: Route) {
        _route.tryEmit(event)
    }

    suspend fun navigateToAsync(event: Route) {
        _route.emit(event)
    }
}

/**
 * This composable LaunchedEffect collects navigation events
 * from the [NavigationManager] and navigates to the next
 * emitted screen.
 */
@Composable
fun NavigatorLaunchedEffect(
    navController: NavHostController,
) {
    LaunchedEffect("NavigationEvents") {
        NavigationManager.route.collect { screen ->
            navController.navigate(screen)
        }
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface {
                    val navController = rememberNavController()

                    // Listen and respond to navigation events.
                    NavigatorLaunchedEffect(navController = navController)

                    NavHost(navController = navController, startDestination = Route.Home) {
                        composable<Route.Home> {  
                            HomeScreen()
                        }
                        composable<Route.Screen1> {
                            Screen1()
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun HomeScreen() {
    Button(
        onClick = {
            NavigationManager.navigateTo(Route.Screen1)
        }
    ) {
        Text("Navigate to Screen1")
    }
}

@Composable
fun Screen1() {
    Text("Screen1")
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文