缘起
最近发现 scheme
组件使用的一些不完美和可改进点,主要有以下几个:
DeepLink
该如何支持?- 期望使用时可以获取结构化的数据(
data class
),避免从NavBackStackEntry
去getString
、getInt
之类的。 - 期望有更好的转场动画支持。
对于 DeepLink
而言,因为 scheme
本来就是 uri
的结构,所以我建议的方案是用一个透明的 Activity
做中转,把 protocol
和 host
部分一下,就是可以用来接入 scheme
框架了,所以本文不做过多分析。
所以最新更新的 0.8.0
主要是为了解决传参结构化和转场动画问题。
结构化传参与解析
目前 scheme
提供的传参方式主要是 Bundle
式的原始方案:在传参需要使用 schemeBuilder.arg(name, value)
的形式链式拼接,而使用时则需要从 NavBackStackEntry
的 arguments
中去一个个的取出来,所以这里存在 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
的过程。
要实现这个方案,我们有两种选择:
- 反射:
Encode
通过反射得到class
下的所有字段名和值,来拼接字符串。Decode
通过将字符串解析成Map
, 再反射赋值给class
。 - 代码生成:通过
ksp
为每个class
生成相应的Encode/Decode
方法实现
为了性能考虑,一般我们会选择代码生成的方案,不过我们并不需要从零开始去设计一套方案,因为我们已经有了强大的 kotlin-serialization
。 因为这本身也是一个序列化反序列化的过程,只不过我们这里只是序列化成了 url query
的形式。大家一般都是用了 kotlin-serialization-json
来做 json
的序列化,其实大家不知道是它还可以被序列化成 protobuf
、cbor
等形式,抽象是做得相当好的了。
使用
首先,定义参数类
// 只支持 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
框架底层依赖的是 accompanist
的 Navigation
库,其本身就有提供高度自定义化的动画支持。其函数签名为:
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
)
其就包括了 enter
、exit
、popEnter
、popExit
四个动作场景的动画,在旧版本,虽然有提供动画自定义,但是将原本的功能给阉割了部分,而新版本虽然使用上不算完美,但保留了其全部自定义的能力。
基础知识
如果我们使用过 Fragment
,那么你肯定对动画的这四个动作很熟悉。但是,两者的名字相同,但代表的意义并不一致。
Fragment
启动一个新的界面,是开启了一个事务,然后在这个事务中,规定新旧界面的动画, 假设有界面 A
和 B
:
- 从
A
切换到B
, 对B
应用enter
, 对A
应用exit
- 从
B
返回到A
, 对B
应用popExit
, 对A
应用popEnter
简单记忆就是 1,4
参数应用新界面, 2,3
参数应用旧界面。
但是到了 Compose
情况就不一样了,Compose
是声明式,用状态描述一切,composable
是为当前声明注册了四个动画描述,用于在不同状态切换时使用不同动画,所以这四个动画都只与注册的 Composable
函数相关。所以:
- 从
A
切换到B
, 对B
应用B
的enter
, 对A
应用A
的exit
- 从
B
返回到A
, 对B
应用B
的popExit
, 对A
应用A
的popEnter
因为动画是提前注册好的,所以会存在一个问题,例如 A
可能跳转 B
, 也可能跳转 C
, 那么跳转时都是应用 A
的 exit
, 那我如果期望一个使用 slide
动画,一个使用 fade
动画该怎么办呢?
仔细观察上面函数的签名,就会发现我们注册时注册的不是动画本身,而是要求传入一个 lambda
函数,其函数的返回值才是动画。所以我们是在不同场景都重新构一个动画,那具体的场景我们该怎么区分呢?
答案就存在这个 lambda
函数是在 AnimatedContentScope<NavBackStackEntry>
域下执行的,这个可以拿到动画 initialState
和 targetState
,具体而言就是新旧界面的 NavBackStackEntry
。 如此就可以根据其做出区分。
其实在原本框架上,NavBackStackEntry
的区分能力还是一般,但是如果使用 scheme
框架的话,那就可以拿到更多的区分信息
// 拿到 scheme
fun NavBackStackEntry.readOriginScheme()
// 拿到 scheme transition 的声明,具体含义可见下一节
NavBackStackEntry.readTransition()
// 拿到 scheme 的 action
fun NavBackStackEntry.readAction()
通过这些信息,我们就可以执行丰富的判断。
在了解了这长长的基础后,我们就可以来看看在 scheme
的注解下,该怎么自定义动画。
scheme 转场动画使用
注解 ActivityScheme
和 ComposeScheme
都有一个字段叫 transition
, 其类型是 int
, 指明使用哪一个 SchemeTransitionProvider
,框架提供了几个默认实现:
SchemeTransition.PUSH
: 常规模式,从右边进入, iOS 式命名SchemTransition.PRESENT
: 从底部升起, iOS 式命名SchemTransition.SCALE
: 缩放进入SchemTransition.PUSH_THEN_STILL
: 从右边进入,exit
和popEnter
保持静止,如果从当前界面去往其它界面会有非push
行为,那么就需要使用这个或者完全自定义。
如果你有自定义需求,那么可以往 SchemeTransitionProviders
中注册新的类型与实现
object SchemeTransitionProviders{// 开发者注册的 type 需要大于 0fun put(type: Int, provider: SchemeTransitionProvider)fun get(type: Int): SchemeTransitionProvider
}
SchemeTransitionProvider
是我们自定义需要实现的接口:
interface SchemeTransitionProvider {// 当以 `activity` 进入时需要提供的资源fun activityEnterRes(): Intfun activityExitRes(): Intfun enterTransition(): (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?fun exitTransition(): (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?fun popEnterTransition(): (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?fun popExitTransition(): (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?
}
需要说明的是,因为我的 scheme
是支持 Activity
和 Compose
各种搭配乱跳的,所以需要提供 activity
的转场动画,但它是事务型的,是服务于新旧两个界面的。
而其它的几个方法,详细在了解了上一节的基础知识后,也都了解了具体是做什么的了。
那为何说是不那么完美的呢?
其实最好的写法是直接在 ComposeScheme
或 ActivityScheme
中指明 SchemeTransitionProvider
, 例如
@ComposeScheme(action = "action",alternativeHosts = [MainActivity::class],transition = PushSchemeTransitionProvider::class,
)
@Composable
fun SchemeModelPage(navBackStackEntry: NavBackStackEntry, arg: DataArg){ // 注意顺序不能变更}
这样就不需要再搞一个 int
,然后去注册了。
那为何没有用这种形式呢? 主要是因为 SchemeTransitionProvider
依赖了 AnimatedContentScope
与 NavBackStackEntry
,而它们又不是纯粹的 java
库,在 ksp
库中无法引入,或者有实现方案,但是我不知道?如果有了解的,欢迎交流。 我也可以用 KClass<*>
,不指明类型,运行时再检查,就像上面 alternativeHosts
做的那样,但是问题就是无法写默认值,每写一个界面就指定一个 transition
, 也有点蛋疼。所以目前我采取的这种注册式的折中方案。
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
全套视频资料:
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓
PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题