目录

好玩的 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 和 函数作为构建器 的思想。