好玩的 Kotlin
前言
又好久好久没更新了,主要是最近研究的东西对我来说都比较难,内功不够 的情况下,写不出东西来。
C/C++ 被我暂时搁置了
最近本来是打算捡起来 C++ 的(也 Hello World 了一下 Rust,这个语言感觉对于以前写 Java 的人来说, 看起来理解起来都好别扭啊···)。
看了两天,感觉很枯燥,也没有什么练手项目,盲目的学没有什么意义。而且 C++ 的代码风格,人和人又不同,关键字又很多,最终没能沉下心来看完,暂且搁置了。
看了 Jetpack Compose
同时,研究了一个星期的 Jetpack Compose,我觉得这个是挺爽的,客户端终于前进到了声明式 UI 了!
终于不用写 xml,然后再 findViewById,再 setLayoutParams/setVisibility 了,虽然前端把这些都已经玩烂掉了···
感觉客户端好像全是劣势
突然感觉客户端淡淡的忧桑:
- 1.开发效率低,代码要发版
- 2.crash 了就只能抱头痛哭,或者发 patch
- 3.不断被前端侵蚀,无论是 Hybrid 还是直接跨端。这也是因为上面两点的效率问题吧,H5 都可以完美搞定。唯一能吐槽 H5 的就是说体验不好。但是,其实你看微信小程序,体验很差么?不觉得。
快要没饭碗了😭,所以是不是研究一波前端呢?是不是又学的太杂了,一顿乱看,我需要一门绝活···
结果是 Compose 没看透
本来想写 Compose 的渲染过程的,但是发现自己理解不够深刻(怕写错),而且视角不够宏观,没有彻底搞懂这个东西的设计,只是靠 debug 断点在猜测整个渲染流程,所以没敢下笔···
主要精髓:就是将 View 的参数和描述(改变的方式) 转换成了一个函数,然后通过观察数据变化,重新执行这个函数。
当然,这其中肯定有很多复杂的逻辑,而且许多代码是 编译时生成的,具体的源码一时半会了解不全。 同时,渲染过程也重写了(也没完全重写,用了一个空壳的 View,硬件加速的情况下,基于 RenderNode 来搞),也包括事件分发的代码修改等等···
总之,内功不够,理解不透,暂时还没发纵览全局,也就下不了笔。
而且 Kotlin 不太熟,也给我看源码构成了一些小障碍!
那现在新的 Android 的东西都是基于 Kotlin 来写的,与其研究 C/C++、Vue/React,不如先好好学学 Kotlin,而且我的 Kotlin 功底挺差的。
好在有一个 Kotlin 大神同事在,偷偷看他写的代码,非常的 Kotlin Style,默默的学了学。
Kotlin Functions Are First-Class
比如,我看到了如下的代码:
private fun GrowthTestLayout.inflate(context: Context) = this.apply {
space()
title("应用信息") {
inflateAppInfo(context)
}
title("手机信息") {
inflateMobileInfo(context)
}
title("设备重置") {
action("设置新机(真机体验) >>") { context.startTestNewDeviceActivity() }
action("清除应用数据") { TestNewDeviceUtils.showClearDataDialog(context) }
action("手机垃圾清理") { context.startGrowthCleanerActivity() }
}
}
这个就很有意思了,写起来清晰多了,这里是一个 RecyclerView,一个 function 对应一个 item,并且会 inflate 对应的布局文件。当然,同事也为了更通用,声明了 action 等许多函数,这里的 action 就是最终当 onClick 函数传了过去。
能轻松的做这些,也是因为在 Kotlin 中函数可以作为一等公民了,引用 Kotlin 官方文档的描述(也有中文的,但是感觉翻译的有点诡异):
Kotlin functions are first-class, which means that they can be stored in variables and data structures, passed as arguments to and returned from other higher-order functions. You can operate with functions in any way that is possible for other non-function values. To facilitate this, Kotlin, as a statically typed programming language, uses a family of function types to represent functions and provides a set of specialized language constructs, such as lambda expressions.
Passing Trailing Lambdas
这个翻译的叫做 传递末尾的 lambda 表达式,很多地方使用到。 就是如果一个函数的最后一个参数类型是个 函数,那么这个函数可以放到圆括号之外。 eg:
// 函数声明
fun test(param1: String, param2: () -> Unit) {
}
// 函数使用
test("param1") {
// 函数体
}
如果有且仅有一个参数且是函数类型,圆括号都可以省略。
// 函数声明
fun test(param: () -> Unit) {
}
// 函数使用
test{
// 函数体
}
同时,记住 it:单个参数的隐式名称
Type-safe Builders
看了半天 Kotlin 文档,发现了 Type-safe Builders 这个章节,发现举例代码很像同事写的代码,原理其实依然是依靠之前说的 functions are first-class 这一点。
同时 Kotlin 也支持 Extension Functions,就是可以给一个类增加一个额外方法。 这个我依稀的记得 OC 一直都有这个功能?对于 C 系语言,怎么实现的这个呢?反正在 Kotlin 中这只是个 语法糖 而已。
Type-safe Builders 章节举例了一些 DSL,比如 HTML 和 Anko 这两个库。
Anko 也是一开始用于摆脱 XML 的,但是现在发展成啥样了不是很清楚。
我也练了练手,自己写了个 极其简陋且丑陋 的 DSL,应该是类似 Anko 的,我并没有看它的源码哈:
row(this) {
layout {
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams
.WRAP_CONTENT
)
}
text {
text = "hello world 1"
textSize = 16f
gravity = Gravity.CENTER
}
}
不知道最顶层的 context 怎么处理,所以我干脆传了个 this(Activity) 进去。
最好玩的地方在于 row 这个函数是怎么定义的
class Row(context: Context) : ViewGroupDSL(context) {
init {
orientation = HORIZONTAL
}
}
// 其实就是个 LinearLayout
open class ViewGroupDSL(context: Context) : LinearLayout(context) {
fun layout(function : () -> LinearLayout.LayoutParams) {
layoutParams = function()
}
}
// row 函数
fun row(context: Context, body: Row.() -> Unit):Row {
val row = Row(context)
row.body()
return row
}
这里第一个参数是 Context,其实 Row extends LinearLayout 所以需要一个 Context 作为参数 最后一个参数是个 Row 的 Extension Function,同时根据前面说的 Trailing Lambda,函数体可以被放到外面(且 this 是 Row):
row(params1) {
// 函数体
// this = Row
}
```kotlin
又因为 this 是 Row,又可以给 Row 增加几个方法,比如加一个 text 方法(这里不知道 params 怎么传过去比较好,自己写了一个很挫的办法:还是传 function):
```kotlin
// 加一个通用的 text 方法
open class ViewGroupDSL(context: Context) : LinearLayout(context) {
// 增加一个 text 方法
fun text(body: Text.() -> Unit): Text {
val textView = Text(context)
textView.body()
addView(textView)
return textView
}
}
// Row extends ViewGroupDSL extends LinearLayout
// orientation HORIZONTAL
class Row(context: Context) : ViewGroupDSL(context) {
init {
orientation = HORIZONTAL
}
}
class Text(context: Context) : AppCompatTextView(context) {
// 加一个 click 方法
fun click(onclick : (View) -> Unit) {
setOnClickListener(onclick)
}
// 加一个 layout 方法
fun layout(function : () -> LinearLayout.LayoutParams) {
layoutParams = function()
}
}
于是我就可以这样调用
row(params1, params2) {
// row 本身的参数,感觉这么传参数很挫
layout {
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
}
// text 方法
text {
// this 就是 TextView
text = "hello world 1"
textSize = 20f
gravity = Gravity.CENTER
// text 的 layout params,感觉很挫
layout {
val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout
.LayoutParams.WRAP_CONTENT
)
layoutParams.gravity = Gravity.CENTER
layoutParams
}
// text 的 click 方法
click {
Toast.makeText(context, "hello world", Toast.LENGTH_SHORT).show()
}
}
}
除了 layout 方法,其他的代码看起来是不是还挺有意思的?
当然了,我这只是最简单的情况,其实想完全处理 XML 布局的情况这远远不够,还有包括各种父布局(ConstraintLayout 等)。
但是已经不是很关心具体的实现了,具体的实现都是设计思想的体现,原理已经搞明白了。Anko 肯定有更好的设计,但是源码我并没有看,也不是很想看了(东西太多,学不过来了,而且我觉得 Compose 应该是个趋势吧?有这时间继续看 Compose 了)
更多的思考
既然可以实现这种布局的 DSL,那么也可以完全抛开 Android 的 ViewGroup,使用基础的 YogaLayout,然后围绕 Yoga 搞一套 DSL,像写前端 Flexbox 布局一样来写 Android 布局代码岂不是更有意思?
而且因为 Yoga 的计算方式,布局层级基本只有一层,简直完美,也不用处理 Android 各种父布局参数,统一使用 Flexbox 参数。
总结
主要是上手练了一下 Kotlin Function 相关的东西。 了解了 Trailing Lambda 和 函数作为构建器 的思想。