emo-scheme 新特性

简介: 最近群友指出了 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

目录
相关文章
|
7月前
|
JavaScript 前端开发
Google Earth Engine(GEE)扩展——ee-polyfill JavaScript 方法 (ES6+)
Google Earth Engine(GEE)扩展——ee-polyfill JavaScript 方法 (ES6+)
45 1
|
人工智能 算法 Java
【Scheme】编程学习(一) —— 概述
Scheme 是一种编程语言,为 Lisp 的一种变体,本文概述 Scheme 语言
231 0
|
Go API 开发工具
Go Module 语义化版本规范
Go Module 语义化版本规范
133 0
|
前端开发 API
webpack配置篇(三十八):语义化版本(Semantic Versioning)规范格式
webpack配置篇(三十八):语义化版本(Semantic Versioning)规范格式
122 0
webpack配置篇(三十八):语义化版本(Semantic Versioning)规范格式
|
存储 前端开发 Swift
Swift实用小册19:Extension扩展的使用
在本章中,你将学会Extension扩展的使用方法。
406 0
Swift实用小册19:Extension扩展的使用
|
JavaScript 前端开发
Google Earth Engine(GEE)——JavaScript基本功能介绍(filter过滤)
Google Earth Engine(GEE)——JavaScript基本功能介绍(filter过滤)
285 0
|
缓存 API 数据库
Reading:一款不错的Material Desgin风格的Kotlin版本的开源APP
版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/81946831 Reading https://github.com/Hankkin/Reading 简介 很久不见,重新拾起博客吧,今天分享一个开源APP-Reading,项目的初衷是因为自己平时翻文章,但是公众号又经常忘记看,自己索性搞一个APP吧,顺便练一下Kotlin。
1403 0