emo-scheme 新特性

文章探讨了如何改进Scheme组件,支持DeepLink的结构化数据传递和更好的转场动画。提出使用透明Activity处理DeepLink,通过反射或代码生成实现参数类的序列化和反序列化。同时,介绍了异常处理策略以及如何利用NavBackStackEntry和kotlin-serialization实现更灵活的动画效果。文章还讨论了自定义SchemeTransitionProvider的方法,以适应不同场景的转场需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

缘起

最近发现 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, 也有点蛋疼。所以目前我采取的这种注册式的折中方案。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

在这里插入图片描述
二、源码解析合集
在这里插入图片描述

三、开源框架合集
在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值