在线客服

emo-scheme 新特性

adminadmin 报建百科 2024-04-24 113 9
emo-scheme 新特性

缘起

最近群友指出了 scheme 组件使用的一些不完美和可改进点,主要有以下几个:

  1. DeepLink 该如何支持?
  2. 期望使用时可以获取结构化的数据(data class),避免从 NavBackStackEntrygetStringgetInt 之类的。
  3. 期望有更好的转场动画支持。

对于 DeepLink 而言,因为 scheme 本来就是 uri 的结构,所以我建议的方案是用一个透明的 Activity 做中转,把 protocolhost 部分一下,就是可以用来接入 scheme 框架了,所以本文不做过多分析。

所以最新更新的 0.8.0 主要是为了解决传参结构化和转场动画问题。

结构化传参与解析

目前 scheme 提供的传参方式主要是 Bundle 式的原始方案:在传参需要使用 schemeBuilder.arg(name, value) 的形式链式拼接,而使用时则需要从 NavBackStackEntryarguments 中去一个个的取出来,所以这里存在 name 的管理,而且你还需要记住不同的 name 对应的 value 的类型

@ComposeScheme(
    action = SchemeConst.ACTION_HOME,
    alternativeHosts = [HomeActivity::class]
)
@Composable
fun HomePage(navBackStackEntry: NavBackStackEntry) {
  val a = navBackStackEntry.arguments?.getString("nameA")
  val b = navBackStackEntry.arguments?.getInt("nameB")
}

而结构化传参则期望我传递给 Composable 函数的就是结构化的数据

@ComposeScheme(
    action = "action",
    alternativeHosts = [MainActivity::class]
)
@Composable
fun SchemeModelPage(arg: DataArg){ 
}

因为我们参数会以 url query 的形式传递,实际上我们就需要实现一个 Encode/Decode 的过程。

要实现这个方案,我们有两种选择:

  1. 反射:Encode 通过反射得到 class 下的所有字段名和值,来拼接字符串。Decode 通过将字符串解析成 Map, 再反射赋值给 class
  2. 代码生成:通过 ksp 为每个 class 生成相应的 Encode/Decode 方法实现

为了性能考虑,一般我们会选择代码生成的方案,不过我们并不需要从零开始去设计一套方案,因为我们已经有了强大的 kotlin-serialization。 因为这本身也是一个序列化反序列化的过程,只不过我们这里只是序列化成了 url query 的形式。大家一般都是用了 kotlin-serialization-json 来做 json 的序列化,其实大家不知道是它还可以被序列化成 protobufcbor 等形式,抽象是做得相当好的了。

使用

首先,定义参数类

// 只支持 bool,int,long,float,string 这几个类型
// 可以享受 Kotlin 的默认值
@Serializable
data class DataArg(
    val i: Int = 3,
    val l: Long = 4,
    val b: Boolean = true,
    val str: String = "xixi"
)

scheme 构建可以从参数类中构建

val arg = DataArg(str = "hehe")
// 通过传递给 SchemeBuilder 的 model 来构建 scheme
val scheme = schemeBuilder.model(arg).toString()

然后就可以在 Composable 方法上直接使用了

@ComposeScheme(
    action = "action",
    alternativeHosts = [MainActivity::class]
)
@Composable
fun SchemeModelPage(arg: DataArg){ // 直接将参数类传递给 Composable 函数就行
    
}

如果你需要使用到 NavBackStackEntry, 那也可以写到方法里


@ComposeScheme(
    action = "action",
    alternativeHosts = [MainActivity::class]
)
@Composable
fun SchemeModelPage(navBackStackEntry: NavBackStackEntry, arg: DataArg){ // 注意顺序不能变更
    
}

当然你可以不使用这一特性,旧版本的工作方式依旧能正常工作。

异常处理

由于引入了序列化与反序列化,就有一些更多不可控的因素。例如使用了 scheme 不支持的类型,如列表等。还有反序列化失败等。

如果有异常那就崩溃,那体验就不好了。 如果把异常全都吞掉,那开发查问题就太难了。所以这里关键倚靠的是 EmoConfig.debug 的值了:

  • 如果值为 true, 那就会直接抛出异常,直接 crash
  • 如果值为 false, 那就会吞掉异常,具体表现为:
    • 如果是从参数类中构建 scheme 时失败了,那这个 scheme 不会触发跳转。
    • 如果从 scheme 中解析参数类失败了,那就视 Composable 函数签名而定了: 如果 Composable 函数指定参数可空 即声明为 fun SchemeModelPage(arg: DataArg?),则函数获得的实参为 null,交给开发者自己去处理这种情况;如果声明了不可空,则 Composable 函数不会被调用,用户侧可能就看到白屏了。

动画

scheme 框架底层依赖的是 accompanistNavigation 库,其本身就有提供高度自定义化的动画支持。其函数签名为:

public fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)? = null,
    exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)? = null,
    popEnterTransition: (
        AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?
    )? = enterTransition,
    popExitTransition: (
        AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?
    )? = exitTransition,
    content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
)

其就包括了 enterexitpopEnterpopExit 四个动作场景的动画,在旧版本,虽然有提供动画自定义,但是将原本的功能给阉割了部分,而新版本虽然使用上不算完美,但保留了其全部自定义的能力。

基础知识

如果我们使用过 Fragment,那么你肯定对动画的这四个动作很熟悉。但是,两者的名字相同,但代表的意义并不一致。

Fragment 启动一个新的界面,是开启了一个事务,然后在这个事务中,规定新旧界面的动画, 假设有界面 AB

  • A 切换到 B, 对 B 应用 enter, 对 A 应用 exit
  • B 返回到 A, 对 B 应用 popExit, 对 A 应用 popEnter

简单记忆就是 1,4 参数应用新界面, 2,3参数应用旧界面。

但是到了 Compose 情况就不一样了,Compose 是声明式,用状态描述一切,composable 是为当前声明注册了四个动画描述,用于在不同状态切换时使用不同动画,所以这四个动画都只与注册的 Composable 函数相关。所以:

  • A 切换到 B, 对 B 应用 Benter, 对 A 应用 Aexit
  • B 返回到 A, 对 B 应用 BpopExit, 对 A 应用 ApopEnter

因为动画是提前注册好的,所以会存在一个问题,例如 A 可能跳转 B, 也可能跳转 C, 那么跳转时都是应用 Aexit, 那我如果期望一个使用 slide 动画,一个使用 fade 动画该怎么办呢?

仔细观察上面函数的签名,就会发现我们注册时注册的不是动画本身,而是要求传入一个 lambda 函数,其函数的返回值才是动画。所以我们是在不同场景都重新构一个动画,那具体的场景我们该怎么区分呢?

答案就存在这个 lambda 函数是在 AnimatedContentScope<NavBackStackEntry> 域下执行的,这个可以拿到动画 initialStatetargetState,具体而言就是新旧界面的 NavBackStackEntry。 如此就可以根据其做出区分。

其实在原本框架上,NavBackStackEntry 的区分能力还是一般,但是如果使用 scheme 框架的话,那就可以拿到更多的区分信息

// 拿到 scheme
fun NavBackStackEntry.readOriginScheme()
// 拿到 scheme transition 的声明,具体含义可见下一节
NavBackStackEntry.readTransition()
// 拿到 scheme 的 action
fun NavBackStackEntry.readAction()

通过这些信息,我们就可以执行丰富的判断。

在了解了这长长的基础后,我们就可以来看看在 scheme 的注解下,该怎么自定义动画。

scheme 转场动画使用

注解 ActivitySchemeComposeScheme 都有一个字段叫 transition, 其类型是 int, 指明使用哪一个 SchemeTransitionProvider,框架提供了几个默认实现:

  • SchemeTransition.PUSH: 常规模式,从右边进入, iOS 式命名
  • SchemTransition.PRESENT: 从底部升起, iOS 式命名
  • SchemTransition.SCALE: 缩放进入
  • SchemTransition.PUSH_THEN_STILL: 从右边进入,exitpopEnter 保持静止,如果从当前界面去往其它界面会有非 push 行为,那么就需要使用这个或者完全自定义。

如果你有自定义需求,那么可以往 SchemeTransitionProviders 中注册新的类型与实现

object SchemeTransitionProviders{
    // 开发者注册的 type 需要大于 0
    fun put(type: Int, provider: SchemeTransitionProvider)
    fun get(type: Int): SchemeTransitionProvider
}

SchemeTransitionProvider 是我们自定义需要实现的接口:

interface SchemeTransitionProvider {
    // 当以 `activity` 进入时需要提供的资源
    fun activityEnterRes(): Int
    fun activityExitRes(): Int
    fun enterTransition(): (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?
    fun exitTransition(): (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?
    fun popEnterTransition(): (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?
    fun popExitTransition(): (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?
}

需要说明的是,因为我的 scheme 是支持 ActivityCompose 各种搭配乱跳的,所以需要提供 activity 的转场动画,但它是事务型的,是服务于新旧两个界面的。

而其它的几个方法,详细在了解了上一节的基础知识后,也都了解了具体是做什么的了。

那为何说是不那么完美的呢?

其实最好的写法是直接在 ComposeSchemeActivityScheme 中指明 SchemeTransitionProvider, 例如

@ComposeScheme(
    action = "action",
    alternativeHosts = [MainActivity::class],
    transition = PushSchemeTransitionProvider::class,
)
@Composable
fun SchemeModelPage(navBackStackEntry: NavBackStackEntry, arg: DataArg){ // 注意顺序不能变更
    
}

这样就不需要再搞一个 int ,然后去注册了。

那为何没有用这种形式呢? 主要是因为 SchemeTransitionProvider 依赖了 AnimatedContentScopeNavBackStackEntry,而它们又不是纯粹的 java 库,在 ksp 库中无法引入,或者有实现方案,但是我不知道?如果有了解的,欢迎交流。 我也可以用 KClass<*>,不指明类型,运行时再检查,就像上面 alternativeHosts 做的那样,但是问题就是无法写默认值,每写一个界面就指定一个 transition, 也有点蛋疼。所以目前我采取的这种注册式的折中方案。


我是古哥E下,前微信读书客户端程序猿 / 自学 5 年中医,维护过上万 Star 开源项目 QMUI Android,现独立维护好用简洁的 Android 组件库 emo

关注我可得:ChatGPT 开发玩法 | 程序员学习经验 | 组件库新变动 | 中医健康调理 。

emo官网:emo.qhplus.cn

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!
代办报建

本公司承接江浙沪报建代办施工许可证。
联系人:张经理,18321657689(微信同号)。

喜欢0发布评论

9条评论

  • 游客 发表于 2个月前

    太邪乎了吧?https://sdceda.com/lao/787806991/

  • 8001直播 发表于 2个月前

    好东西,赞一个!http://v79j3e.0168333.com

发表评论

  • 昵称(必填)
  • 邮箱
  • 网址