|
@@ -0,0 +1,194 @@
|
|
|
|
|
+package com.hzliuzhi.applet.core.router
|
|
|
|
|
+
|
|
|
|
|
+import android.os.Parcelable
|
|
|
|
|
+import androidx.compose.animation.AnimatedContentScope
|
|
|
|
|
+import androidx.compose.runtime.Composable
|
|
|
|
|
+import androidx.navigation.NamedNavArgument
|
|
|
|
|
+import androidx.navigation.NavBackStackEntry
|
|
|
|
|
+import androidx.navigation.NavDeepLink
|
|
|
|
|
+import androidx.navigation.NavGraphBuilder
|
|
|
|
|
+import androidx.navigation.NavHostController
|
|
|
|
|
+import androidx.navigation.NavType
|
|
|
|
|
+import androidx.navigation.compose.composable
|
|
|
|
|
+import androidx.navigation.navArgument
|
|
|
|
|
+import androidx.navigation.navDeepLink
|
|
|
|
|
+import java.util.concurrent.ConcurrentHashMap
|
|
|
|
|
+import kotlin.reflect.KProperty1
|
|
|
|
|
+import kotlin.reflect.KType
|
|
|
|
|
+import kotlin.reflect.full.createInstance
|
|
|
|
|
+import kotlin.reflect.full.memberProperties
|
|
|
|
|
+import kotlin.reflect.full.primaryConstructor
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// region 反射缓存
|
|
|
|
|
+val constructorCache = ConcurrentHashMap<Class<*>, kotlin.reflect.KFunction<*>>()
|
|
|
|
|
+val memberPropertiesCache = ConcurrentHashMap<Class<*>, Collection<KProperty1<out Any, *>>>()
|
|
|
|
|
+
|
|
|
|
|
+inline fun <reified T : Any> getPrimaryConstructor(): kotlin.reflect.KFunction<*>? {
|
|
|
|
|
+ val key = T::class.java
|
|
|
|
|
+ constructorCache[key]?.let { return it }
|
|
|
|
|
+ val ctor = T::class.primaryConstructor
|
|
|
|
|
+ if (ctor != null) {
|
|
|
|
|
+ constructorCache[key] = ctor
|
|
|
|
|
+ }
|
|
|
|
|
+ return ctor
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@Suppress("UNCHECKED_CAST")
|
|
|
|
|
+inline fun <reified T : Any> getMemberProperties(): Collection<KProperty1<T, *>> {
|
|
|
|
|
+ val key = T::class.java
|
|
|
|
|
+ memberPropertiesCache[key]?.let { return it as Collection<KProperty1<T, *>> }
|
|
|
|
|
+ val props = T::class.memberProperties as Collection<KProperty1<T, *>>
|
|
|
|
|
+ memberPropertiesCache[key] = props
|
|
|
|
|
+ return props
|
|
|
|
|
+}
|
|
|
|
|
+// endregion
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 根据Kotlin类型自动推断NavType,支持基础类型。
|
|
|
|
|
+ * @param type Kotlin反射类型
|
|
|
|
|
+ * @return 对应的NavType
|
|
|
|
|
+ */
|
|
|
|
|
+fun navTypeFor(type: KType): NavType<*> = when (val classifier = type.classifier) {
|
|
|
|
|
+ String::class -> NavType.StringType
|
|
|
|
|
+ Int::class -> NavType.IntType
|
|
|
|
|
+ Long::class -> NavType.LongType
|
|
|
|
|
+ Float::class -> NavType.FloatType
|
|
|
|
|
+ Boolean::class -> NavType.BoolType
|
|
|
|
|
+ else -> NavType.StringType
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 自动生成navArgument,支持部分字段有默认参数。
|
|
|
|
|
+ * 只为有默认值的参数设置defaultValue,必填参数不设置defaultValue。
|
|
|
|
|
+ * @return 包含所有参数的NamedNavArgument列表
|
|
|
|
|
+ */
|
|
|
|
|
+inline fun <reified T : Any> navArgumentsWithDefault(): List<NamedNavArgument> = T::class.run {
|
|
|
|
|
+ val params = getPrimaryConstructor<T>()?.parameters.orEmpty()
|
|
|
|
|
+ // 只尝试获取有默认值的参数的默认实例
|
|
|
|
|
+ val defaultInstance = try {
|
|
|
|
|
+ if (params.all { it.isOptional || it.isVararg }) createInstance() else null
|
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
|
+ null
|
|
|
|
|
+ }
|
|
|
|
|
+ val memberProps = getMemberProperties<T>()
|
|
|
|
|
+ params.map { param ->
|
|
|
|
|
+ val paramName = param.name!!
|
|
|
|
|
+ val type = navTypeFor(param.type)
|
|
|
|
|
+ // 只为有默认值的参数设置 defaultValue
|
|
|
|
|
+ val hasDefault = param.isOptional || (defaultInstance != null && memberProps.find { it.name == paramName }?.getter?.call(defaultInstance) != null)
|
|
|
|
|
+ val defaultValue = if (hasDefault && defaultInstance != null) {
|
|
|
|
|
+ memberProps.find { it.name == paramName }?.getter?.call(defaultInstance)
|
|
|
|
|
+ } else null
|
|
|
|
|
+ navArgument(paramName) {
|
|
|
|
|
+ this.type = type
|
|
|
|
|
+ defaultValue?.let { value ->
|
|
|
|
|
+ @Suppress("IMPLICIT_CAST_TO_ANY")
|
|
|
|
|
+ when (type) {
|
|
|
|
|
+ NavType.StringType -> value as? String
|
|
|
|
|
+ NavType.IntType -> value as? Int
|
|
|
|
|
+ NavType.LongType -> value as? Long
|
|
|
|
|
+ NavType.FloatType -> value as? Float
|
|
|
|
|
+ NavType.BoolType -> value as? Boolean
|
|
|
|
|
+ is NavType.EnumType<*> -> value.toString()
|
|
|
|
|
+ is NavType.ParcelableType<*> -> value as? Parcelable
|
|
|
|
|
+ else -> null
|
|
|
|
|
+ }?.let { this.defaultValue = it }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 自动生成路由字符串,如 user?id={id}&name={name}
|
|
|
|
|
+ * @return 路由字符串
|
|
|
|
|
+ */
|
|
|
|
|
+inline fun <reified T : Any> navRoute() = T::class.run {
|
|
|
|
|
+ val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
|
|
|
|
|
+ val params = getPrimaryConstructor<T>()?.parameters.orEmpty()
|
|
|
|
|
+ params.takeIf { it.isNotEmpty() }
|
|
|
|
|
+ ?.joinToString("&", prefix = "$name?") { "${it.name}={${it.name}}" }
|
|
|
|
|
+ ?: name
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+inline fun <reified T : Any> navRoute(instance: T): String = T::class.run {
|
|
|
|
|
+ val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
|
|
|
|
|
+ val memberProps = getMemberProperties<T>()
|
|
|
|
|
+ val params = memberProps.mapNotNull { prop ->
|
|
|
|
|
+ prop.get(instance)?.let { "${prop.name}=$it" }
|
|
|
|
|
+ }
|
|
|
|
|
+ params.takeIf { it.isNotEmpty() }
|
|
|
|
|
+ ?.joinToString("&", prefix = "$name?")
|
|
|
|
|
+ ?: name
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * NavBackStackEntry自动转为目标data class,支持类型和错误提示
|
|
|
|
|
+ * @throws IllegalArgumentException 参数解析或构造失败时抛出
|
|
|
|
|
+ * @return 目标类型实例
|
|
|
|
|
+ */
|
|
|
|
|
+inline fun <reified T : Any> NavBackStackEntry.toRoute(): T = T::class.run {
|
|
|
|
|
+ val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
|
|
|
|
|
+ // 处理 object 单例类型
|
|
|
|
|
+ T::class.objectInstance?.let { return it as T }
|
|
|
|
|
+ val params = getPrimaryConstructor<T>()?.parameters.orEmpty()
|
|
|
|
|
+ val args = params.associateWith { param ->
|
|
|
|
|
+ val paramName = param.name!!
|
|
|
|
|
+ try {
|
|
|
|
|
+ when (param.type.classifier) {
|
|
|
|
|
+ String::class -> arguments?.getString(paramName)
|
|
|
|
|
+ Int::class -> arguments?.getInt(paramName)
|
|
|
|
|
+ Long::class -> arguments?.getLong(paramName)
|
|
|
|
|
+ Float::class -> arguments?.getFloat(paramName)
|
|
|
|
|
+ Boolean::class -> arguments?.getBoolean(paramName)
|
|
|
|
|
+ else -> arguments?.getString(paramName)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
|
+ throw IllegalArgumentException("参数[$paramName] 解析失败: ${e.message}")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ getPrimaryConstructor<T>()!!.callBy(args) as T
|
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
|
+ throw IllegalArgumentException("$name 构造失败: ${e.message}")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * NavGraphBuilder.route扩展,自动注册路由
|
|
|
|
|
+ * @param content 页面内容Composable
|
|
|
|
|
+ * @param deepLinks 可选的DeepLink列表
|
|
|
|
|
+ */
|
|
|
|
|
+inline fun <reified T : Any> NavGraphBuilder.route(
|
|
|
|
|
+ deepLinks: List<NavDeepLink> = emptyList(),
|
|
|
|
|
+ noinline content: @Composable AnimatedContentScope.(Pair<T, NavBackStackEntry>) -> Unit,
|
|
|
|
|
+) = T::class.run {
|
|
|
|
|
+ val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
|
|
|
|
|
+ composable(
|
|
|
|
|
+ route = navRoute<T>(),
|
|
|
|
|
+ arguments = navArgumentsWithDefault<T>(),
|
|
|
|
|
+ deepLinks = deepLinks + listOf(navDeepLink { uriPattern = "${DeepLink().scheme}${navRoute<T>()}" })
|
|
|
|
|
+ ) {
|
|
|
|
|
+ runCatching { content(Pair(it.toRoute(), it)) }.onFailure {
|
|
|
|
|
+ val message = "$name 路由参数解析或页面构建失败: ${it.message}"
|
|
|
|
|
+ android.util.Log.e("log:route", message, it)
|
|
|
|
|
+ throw IllegalStateException(message)
|
|
|
|
|
+ }.getOrNull()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * NavController.router扩展,自动跳转并带参数
|
|
|
|
|
+ * @param route 路由参数对象
|
|
|
|
|
+ * @throws IllegalStateException 跳转失败时抛出
|
|
|
|
|
+ */
|
|
|
|
|
+inline fun <reified T : Any> NavHostController.router(route: T) = T::class.run {
|
|
|
|
|
+ runCatching {
|
|
|
|
|
+ navigate(navRoute(route))
|
|
|
|
|
+ }.onFailure { it ->
|
|
|
|
|
+ val name = simpleName?.replaceFirstChar { it.lowercase() } ?: "unknown"
|
|
|
|
|
+ val message = "$name 跳转失败: ${it.message}"
|
|
|
|
|
+ android.util.Log.e("log:router", message, it)
|
|
|
|
|
+ throw IllegalStateException(message)
|
|
|
|
|
+ }.getOrNull()
|
|
|
|
|
+}
|