回到项目,接着文章列表做文章收藏功能。这里遇到了问题,未登录情况下点击收藏需要先登录再执行收藏逻辑,但是 Compose Navigation 中没有类似 startActivityForResult 的 api ,需要我们自己拓展。
实现思路:
- 为所有的路由添加可选参数 requsetCode
- 页面跳转之前获取当前 BackStackEntry.savedStateHandle
NavResultKey
的 StateFlow - 跳转后的页面在返回前将 result 保存到 previousBackStackEntry.savedStateHandle
NavResultKey
中 ,result 就回发送到第二步的 StateFlow - 清理 savedStateHandle ,删除其中
NavResultKey
对应的 StateFlow 和 result 值
需要注意不能使用 rememberCoroutineScope() 返回的 scope 来 collect StateFlow,跳转到新页面重组之后这个 scope 就无效了,需要使用 ViewModel 的 scope 。
Screen 部分
Screen 部分的改动有两块
1 为每个路由添加可选参数
//新增部分代码
interface OutRoutes //当前模块使用的外部模块路由
abstract class Screen<T:OutRoutes>(private val path:String){
lateinit var outRoutes:T
//默认添加可选参数 NavRequestCodeKey
val argumentsWithRequestCode:List<NamedNavArgument> = mutableListOf<NamedNavArgument>().apply {
addAll(arguments)
add(navArgument(name = NavRequestCodeKey) {
type = NavType.StringType
nullable = true
})
}
//创建带 requestCode 的路由
fun createForResultRoute(requestCode:String,args:Map<String,Any> = emptyMap()):String{
val withRequestCode = mutableMapOf<String,Any>(NavRequestCodeKey to requestCode)
withRequestCode.putAll(args)
return createRoute(withRequestCode)
}
}
2 实现跨模块路由
sealed class HomeScreens(path: String ) : Screen<HomeScreens.ScreenRoutes>(path) {
override val root: String
get() = "home"
object Index : HomeScreens("index")
//screenRoutes 当前模块需要所有外部模块的路由,当前模块中没有其他模块依赖
//所有模块需要在 WanNavHost 中配置,声明成接口在 WanNavHost 中实现
abstract class NavGraph(navController: NavController,screenRoutes: ScreenRoutes) : ScreenNavGraph(navController, Index) {
override val composeScreens: NavGraphBuilder.() -> Unit = {
//it.outRoutes = screenRoutes 将路由设置到所有 Screen 中
composableScreen(Index.also { it.outRoutes = screenRoutes }) {
UiHome(navController = this@NavGraph.navController)
}
}
}
//跳转到 Login 模块的路由
interface ScreenRoutes:OutRoutes {
fun startLoginForResult(requestCode:String):String
}
}
object RouteRequestCode{
const val Login = "RequestLoginForResult"
}
Navigation 部分
NavGraphKtx 中使用 screen.argumentsWithRequestCode 作为参数配置路由,这个参数在配置的时候必须加上。
fun NavGraphBuilder.composableScreen(
screen: Screen<out OutRoutes>,
content: @Composable (NavBackStackEntry) -> Unit
) {
composable(
route = screen.route,
arguments = screen.argumentsWithRequestCode,//←
deepLinks = screen.deepLinks,
content = content
)
}
NavControllerKtx 中添加实现 navigateForResult 需要的代码
//跳转前先取得StateFlow
//stateHandle.getStateFlow<Bundle>(NavResultKey,Bundle.EMPTY)
fun NavController.navigateForResult(
route: String,
interceptors:List<NavInterceptor>? = null,
builder: NavOptionsBuilder.() -> Unit) :StateFlow<Bundle?>? {
val navOptions = navOptions(builder)
//是否有需要拦截的拦截器
val interceptor = interceptors?.find { it.shouldIntercept(route,navOptions,this) }
return if (interceptor != null){
// 调用拦截器导航
interceptor.navigate(navController = this, originalRoute = route)
Log.i("OnNavResult", "[route:$route] navigateForResult has been Intercepted")
null
}else{
val stateHandle = this.currentBackStackEntry!!.savedStateHandle
navigate(route,navOptions)
return stateHandle.getStateFlow<Bundle>(NavResultKey,Bundle.EMPTY)
}
}
//暂时没啥用先加上
@Composable
fun NavController.navigateForResultCompose(
route: String,
interceptors:List<NavInterceptor>? = null,
builder: NavOptionsBuilder.() -> Unit) :State<Bundle?>? = navigateForResult(route,interceptors,builder)?.collectAsState()
//在当先页面的路由参数中获取 requestCode
fun NavController.getRequestCode():String? = this.currentBackStackEntry!!.arguments?.getString(NavRequestCodeKey,null)
fun NavController.setResult(result: Bundle){
val prevEntry = this.previousBackStackEntry
val requestCode = this.getRequestCode()
//没有 previousBackStackEntry 和 requestCode 时是不需要 setResult 的
if (prevEntry != null && requestCode != null){
result.putString(NavRequestCodeKey,requestCode)
prevEntry.savedStateHandle[NavResultKey] = result
}
}
fun NavController.clearForResult(){
this.currentBackStackEntry!!.savedStateHandle.let {
//删除 savedStateHandle.getStateFlow 时创建的 StateFlow
it.remove<Any>(NavResultKey)
//删除 savedStateHandle 中 NavResultKey 保存的 result 值
it[NavResultKey] = null
}
}
VM 和 State 部分
StateFlow 需要在 vm scope 中 collect
abstract class ComposeViewModel:BaseViewModel(){
fun collectState( block:suspend (CoroutineScope)->Unit){
viewModelScope.launch {
block(this)
}
}
}
在 ComposeVmState 中处理 navForResult 的具体逻辑
fun navForResult(
routeBuilder:(String)->String,
requestCode:String,
interceptors: List<NavInterceptor> = listOf(NotFoundInterceptor),
builder: (NavOptionsBuilder.() -> Unit) = {},
onResult:(Bundle,String) -> Unit
){
//创建 route
val route = routeBuilder(requestCode)
//路由并拿到 NavResultKey 对应的 StateFlow
val resultFlow = navController.navigateForResult(route, interceptors, builder) ?: return
//在 viewModel scope 中 collect
viewModel.collectState {
resultFlow.collect{
if (it != null && it.getString(NavRequestCodeKey,null) == requestCode){ //将 result 传递到 onResult 回调
onResult(it, NavResultDataKey)
//清理 savedStateHandle
navController.clearForResult()
}
}
}
}
使用
@Composable
fun WanNavHost(modifier: Modifier = Modifier,navController: NavHostController){
NavHost(modifier= modifier,navController = navController, startDestination = HomeScreens.Index.root){
HomeGraph(navController,object :HomeScreens.ScreenRoutes{
override fun startLoginForResult(requestCode: String): String {
return ProfileScreens.Login.createForResultRoute(requestCode)
}
}).create(this)
}
}
private fun collectArticle(articleId: Int) {
if (user == null){
navForResult(
routeBuilder = HomeScreens.Index.outRoutes::startLoginForResult,
requestCode = RouteRequestCode.Login,
) { resultData,key ->
//Bundle get 时不同数据类型需要使用不同的方法
//具体取值就放到 onResult 中来实现了
if (resultData.getBoolean(key,false)){
realCollectArticle(articleId)
}
}
}else{
realCollectArticle(articleId)
}
}
完整代码见 git
暂时用着还没啥问题,毕竟现在也没啥功能 哈哈