21 实现 Compose Navigation startActivityForResult 功能

295 阅读3分钟

回到项目,接着文章列表做文章收藏功能。这里遇到了问题,未登录情况下点击收藏需要先登录再执行收藏逻辑,但是 Compose Navigation 中没有类似 startActivityForResult 的 api ,需要我们自己拓展。

实现思路:

  1. 为所有的路由添加可选参数 requsetCode
  2. 页面跳转之前获取当前 BackStackEntry.savedStateHandle NavResultKey 的 StateFlow
  3. 跳转后的页面在返回前将 result 保存到 previousBackStackEntry.savedStateHandle NavResultKey 中 ,result 就回发送到第二步的 StateFlow
  4. 清理 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

暂时用着还没啥问题,毕竟现在也没啥功能 哈哈