Jetpack Compose 导航结果

问题描述 投票:0回答:10

我正在使用 Jetpack Navigation 库和 Compose 版本。我正在设置导航,如此处

所示

我希望能够从屏幕 A 导航到屏幕 B。一旦 B 执行某些操作并从返回堆栈中弹出,它将返回屏幕 A 可以访问的结果。

我找到了一种使用 Activity here 来执行此操作的方法,但我想避免创建任何额外的活动并在 compose 中执行此操作。

android android-jetpack android-jetpack-compose android-jetpack-navigation
10个回答
58
投票

从要返回数据的 Composable 中,您可以执行以下操作:

navController.previousBackStackEntry
    ?.savedStateHandle
    ?.set("your_key", "your_value")
navController.popBackStack()

然后,从源 Composable 中,您可以使用

LiveData
监听更改。

val secondScreenResult = navController.currentBackStackEntry
    ?.savedStateHandle
    ?.getLiveData<String>("your_key")?.observeAsState()
...
secondScreenResult?.value?.let {
    // Read the result
}

11
投票

如果您只需要获取一次值,则需要在使用后删除值:

val screenResultState = navController.currentBackStackEntry
    ?.savedStateHandle
    ?.getLiveData<String>("some_key")?.observeAsState()

screenResultState?.value?.let {
    ...
    // make something, for example `viewModel.onResult(it)`
    ...
    //removing used value
    navController.currentBackStackEntry
        ?.savedStateHandle
        ?.remove<String>("some_key")
}

我还在函数中提取了它(对于 JetPack Compose)

@Composable
fun <T> NavController.GetOnceResult(keyResult: String, onResult: (T) -> Unit){
    val valueScreenResult =  currentBackStackEntry
        ?.savedStateHandle
        ?.getLiveData<T>(keyResult)?.observeAsState()

    valueScreenResult?.value?.let {
        onResult(it)
       
        currentBackStackEntry
            ?.savedStateHandle
            ?.remove<T>(keyResult)
    }
}

您可以将其复制到您的项目中并像这样使用:

navController.GetOnceResult<String>("some_key"){
    ...
    // make something
}

5
投票

最上面的答案对于大多数情况来说已经足够好了,但我发现如果你想用

ViewModel
的方法做某事,使用
ViewModel
并不容易。我没有使用
LiveData
Flow
来观察被调用屏幕的结果,而是使用回调来解决这个问题。

希望我的回答可以帮助到一些人。

import androidx.navigation.NavController

/**
 * The navigation result callback between two call screens.
 */
typealias NavResultCallback<T> = (T) -> Unit

// A SavedStateHandle key is used to set/get NavResultCallback<T>
private const val NavResultCallbackKey = "NavResultCallbackKey"

/**
 * Set the navigation result callback on calling screen.
 *
 * @param callback The navigation result callback.
 */
fun <T> NavController.setNavResultCallback(callback: NavResultCallback<T>) {
    currentBackStackEntry?.savedStateHandle?.set(NavResultCallbackKey, callback)
}

/**
 *  Get the navigation result callback on called screen.
 *
 * @return The navigation result callback if the previous backstack entry exists
 */
fun <T> NavController.getNavResultCallback(): NavResultCallback<T>? {
    return previousBackStackEntry?.savedStateHandle?.remove(NavResultCallbackKey)
}

/**
 *  Attempts to pop the controller's back stack and returns the result.
 *
 * @param result the navigation result
 */
fun <T> NavController.popBackStackWithResult(result: T) {
    getNavResultCallback<T>()?.invoke(result)
    popBackStack()
}

/**
 * Navigate to a route in the current NavGraph. If an invalid route is given, an
 * [IllegalArgumentException] will be thrown.
 *
 * @param route route for the destination
 * @param navResultCallback the navigation result callback
 * @param navOptions special options for this navigation operation
 * @param navigatorExtras extras to pass to the [Navigator]
 *
 * @throws IllegalArgumentException if the given route is invalid
 */
fun <T> NavController.navigateForResult(
    route: String,
    navResultCallback: NavResultCallback<T>,
    navOptions: NavOptions? = null,
    navigatorExtras: Navigator.Extras? = null
) {
    setNavResultCallback(navResultCallback)
    navigate(route, navOptions, navigatorExtras)
}

/**
 * Navigate to a route in the current NavGraph. If an invalid route is given, an
 * [IllegalArgumentException] will be thrown.
 *
 * @param route route for the destination
 * @param navResultCallback the navigation result callback
 * @param builder DSL for constructing a new [NavOptions]
 *
 * @throws IllegalArgumentException if the given route is invalid
 */
fun <T> NavController.navigateForResult(
    route: String,
    navResultCallback: NavResultCallback<T>,
    builder: NavOptionsBuilder.() -> Unit
) {
    setNavResultCallback(navResultCallback)
    navigate(route, builder)
}

使用示例:


fun NavGraphBuilder.addExampleGraph(navController: NavController) {
    composable(FirstScreenRoute) {
        FirstScreen(
            openSecondScreen = { navResultCallback ->
                navController.navigateForResult(SecondScreenRoute, navResultCallback = navResultCallback)
            },
            ... // other parameters
        )
    }

    composable(SecondScreenRoute) {
        SecondScreen(
            onConfirm = { result: T ->  // Replace T with your return type
                navController.popBackStackWithResult(result)
            },
            onCancel = navController::navigateUp,
            ... // other parameters
        )
    }
}

1
投票

无需 LiveData 或 Flow 即可获得结果,可以使用

savedStateHandle.remove
方法。我认为这是更简单的方法:

val secondResult = appNavController.currentBackStackEntry?.savedStateHandle?.remove<Data?>("data")
secondResult?.let { data ->
    Log.d(TAG, "Data result: $data")
}

0
投票

对于jetpack compose,您必须使用

Flow
collectAsState
才能获得结果:

navController.currentBackStackEntry
?.savedStateHandle?.getStateFlow<Boolean?>("refresh", false)
?.collectAsState()?.value?.let {
if (it)screenVM.refresh() }

您也可以通过在

screenVM.refresh()
之后添加此内容来删除条目:

 navController.currentBackStackEntry
                ?.savedStateHandle ?.set("refresh", false)

0
投票

添加依赖项

implementation "androidx.compose.runtime:runtime-livedata:$compose_version"

在发件人屏幕上设置一个键值对以发送回呼叫者屏幕,我使用键名称为“key”值为 true 的布尔值

navController.previousBackStackEntry?.savedStateHandle?.set("key", true)

向上导航

navController.navigateUp()

接收方屏幕(来电者)监听结果然后移除:

 val result =  navController.currentBackStackEntry?.savedStateHandle
    ?.getLiveData<Boolean>("key")?.observeAsState()
result?.value?.let {
    navController.currentBackStackEntry?.savedStateHandle
        ?.remove<Boolean>("key")
}

第一个屏幕

@Composable fun FirstScreen(navController: NavController){

val result =  navController.currentBackStackEntry?.savedStateHandle
    ?.getLiveData<Boolean>("key")?.observeAsState()
result?.value?.let {
    navController.currentBackStackEntry?.savedStateHandle
        ?.remove<Boolean>("key")
}

Button(onClick = {
    navController.navigateUp("secondScreen")
}) {
    "Open second screen"
}}

0
投票

如果您想从 PageC 返回到 PageA 并弹出 pageB 而不返回到它,我找到了解决方案:

等待屏幕A的结果

composable("ScreenA") {
  val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
  val result by savedStateHandle.getStateFlow("key").collectAsState()
  ScreenA(result)
}

从screen3返回值

  navController.apply {
            backQueue.firstOrNull { it.destination.route == route }?.savedStateHandle?.set("key",true) //
            popBackStack(route, inclusive)
        }

当你得到你想要的答案后 从 page3 中删除它,因为您将其保存在页面的 saveStateHandle 上

savedStateHandle.remove<Boolean>("key")

0
投票

参考nglauber的答案,我发现我重复观察到相同的结果,直到我从调用

SavedStateHandle.remove()
切换到将
LiveData
的值设置为
null

@Composable
fun <T> NavBackStackEntry.GetOnceResult(resultKey: String, onResult: (T) -> Unit) {
    val resultLiveData = savedStateHandle.getLiveData<T>(resultKey)
    resultLiveData.observeAsState().value?.let {
        resultLiveData.value = null
        onResult(it)
    }
}

看,

SavedStateHandle.getLiveData()
实际上返回一个
MutableLiveData
,而不仅仅是一个通用的
LiveData
。一开始我很惊讶,直到我意识到这一定是故意的,为了你通过
MutableLiveData
修改保存的状态(它实际上是这样做的,而不是简单地修改
LiveData
本身) ).

当我看到

SavedStateHandle.remove()
的文档时,我有了这个想法:

删除与给定键关联的值。如果存在与给定键关联的 LiveData 和/或 StateFlow,它们也将被删除。 之前由 SavedStateHandle.getLiveData 或 getStateFlow 返回的 androidx.lifecycle.LiveDatas 或 StateFlows 的所有更改都不会反映在保存的状态中。此外,LiveData 或 StateFlow 不会收到有关给定键关联的新值的任何更新。

我添加了一些日志记录来确认,正常情况下,每次重组时对

getLiveData()
的调用都会再次返回相同的
LiveData
实例,调用
SavedStateHandle.remove()
会导致它随后返回不同的
LiveData
(这会为您提供旧值) ,导致重复观察)。


0
投票

在第二个屏幕中,您可以像这样发送结果:

navController.previousBackStackEntry?.savedStateHandle?.set("key", true)

在父屏幕中您可以听到如下效果:

navController.currentBackStackEntry?.savedStateHandle?.getStateFlow("result_key", false)?.collectAsEffect {
        it.takeIf {
            it
        }?.also {
            //do something
        }
}

你应该写这个扩展 fun 来收集效果:

@Composable
fun <T> Flow<T>.collectAsEffect(
    context: CoroutineContext = EmptyCoroutineContext,
    block: (T) -> Unit
) {
    LaunchedEffect(key1 = Unit) {
        onEach(block).flowOn(context).launchIn(this)
    }
}

-1
投票
val navController = rememberNavController()
composable("A") {
    val viewmodel: AViewModel = hiltViewModel()
    AScreen()
}
composable("B") {
    val viewmodel: BViewModel = hiltViewModel()
    val previousViewmodel: AViewModel? = navController
        .previousBackStackEntry?.let {
            hiltViewModel(it)
        }
    BScreen(
       back = { navController.navigateUp() },
       backWhitResult = { arg ->
           previousViewmodel?.something(arg)
       }
    )
}
© www.soinside.com 2019 - 2024. All rights reserved.