缘起
最近群友指出了 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 需要大于 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
是支持 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
, 也有点蛋疼。所以目前我采取的这种注册式的折中方案。
我是古哥E下,前微信读书客户端程序猿 / 自学 5 年中医,维护过上万 Star 开源项目 QMUI Android
,现独立维护好用简洁的 Android
组件库 emo
。
关注我可得:ChatGPT
开发玩法 | 程序员学习经验 | 组件库新变动 | 中医健康调理 。
emo官网:emo.qhplus.cn
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!代办报建
本公司承接江浙沪报建代办施工许可证。
联系人:张经理,18321657689(微信同号)。
11条评论
太邪乎了吧?https://sdceda.com/lao/787806991/
好东西,赞一个!http://v79j3e.0168333.com
吹牛的人越来越多了!http://x7koo4.f1po.com
有机会找楼主好好聊聊!http://5yaic.315-xfzsc.com
看帖不回帖都是耍流氓!http://ffa2rn.zxpart.com
内容很有深度!http://yqp.gamaa456.com
鸟大了,什么林子都敢进啊!http://pqjhza.zzcjdq.cn
我只是来赚积分的!http://4tn.09rz.com
鸟大了,什么林子都敢进啊!http://jf7yx.3x80.com
楼主加油,看好你哦!http://e7o6.zhenlong888.cn
好无聊啊!https://www.telegramxp.com/
发表评论