我正在使用 Jetpack Navigation 库和 Compose 版本。我正在设置导航,如此处
所示我希望能够从屏幕 A 导航到屏幕 B。一旦 B 执行某些操作并从返回堆栈中弹出,它将返回屏幕 A 可以访问的结果。
我找到了一种使用 Activity here 来执行此操作的方法,但我想避免创建任何额外的活动并在 compose 中执行此操作。
从要返回数据的 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
}
如果您只需要获取一次值,则需要在使用后删除值:
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
}
最上面的答案对于大多数情况来说已经足够好了,但我发现如果你想用
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
)
}
}
无需 LiveData 或 Flow 即可获得结果,可以使用
savedStateHandle.remove
方法。我认为这是更简单的方法:
val secondResult = appNavController.currentBackStackEntry?.savedStateHandle?.remove<Data?>("data")
secondResult?.let { data ->
Log.d(TAG, "Data result: $data")
}
对于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)
添加依赖项
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"
}}
如果您想从 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")
参考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
(这会为您提供旧值) ,导致重复观察)。
在第二个屏幕中,您可以像这样发送结果:
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)
}
}
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)
}
)
}