D05 / 为什麽不会填错资料? - Inline class, Scope  & DSL design in compose

今天大概会聊到的范围

  • slot api
  • modifier scope
  • inline classes

在研究如何使用 ConstraintLayout 的时候,有一件事情让我觉得很神奇。

在 ConstraintLayout 中的 Composable 的会多一个 constraintAs 的 Modifier 用来描述 constraint。
(来自上一篇)

为什麽我用 ConstraintLayout 时,ConstraintLayout 内的 component 就会多这个 modifier?
同样是之前有聊到的 align,为什麽 Row 里面的 component 的 Modifier.align()Alignment.Vertical,但 Column 中的 Modifier.align() 吃的参数型别却是 Alignment.Horizontal 呢?

其实这个问题很简单,但了解之後却觉得这是非常聪明的设计。

Slot API

首先,可以聊聊 Slot API 的设计。

先看看 Button,最简单的 Button 只需要一个文字,所以 Button 这个 composable 可以这样设计。

@Composable
fun Button(
    text: String
) { 
    // ... 
}

但有时,我们需要改变 Button 的颜色,有时我们需要在文字前放一个 icon,放了 icon 又需要改变 icon 的颜色。最後,Button 的设计就会越来越复杂

@Composable
fun Button(
    text: String,
    icon: Icon,
    iconColor: Color,
    backgroundColor: Color,
    textFontStyle: ...
    // ... 
) { 
    // ... 
}

当然,可以透过很多很多参数去开放这个 Button 的能力,但永远猜不到最终开发者会期待怎样的自由度。於是,Compose 团队的设计是在 Button 这个元件中留一个固定的区块,在这个区块中,让开发者自由发展。

@Composable
fun Button(
    // ... other params
    content: @Composable () -> Unit
) {
    // ... content 会用在这里
}

这样的设计被称为 Slot API。这种设计在 TopAppBar 以及 Scaffold 这样的 Composable 中尤其明显。可以在这里看到更多说明。

Scope

Row、Column 和 Button 一样有类似 Slot API 的设计,在 function 的最後接收了一个名为 content 的 composable function。但仔细看看,发现这些 composable function 不只是普通的 function,而是一个 lambda with receiver。

@Composable
inline fun Row(
    modifier: Modifier = Modifier,
    // ... other params
    content: @Composable RowScope.() -> Unit
) {
    // ... 
}

再来看看 RowScope,里面实作了多个 Modifier 的 extension function。其中,就有 align。

interface RowScope {
    @Stable
    fun Modifier.align(alignment: Alignment.Vertical): Modifier
}

如此,问题就解开了。平常我们写在 Row 底下这个 trailing lambda 的 code 是会因为 lambda with receiver 的机制 run 在 RowScope 底下,在 RowScope 底下,又有特化的 Modifier 并且只接受特别的参数。因此,Row 中的 component 使用 align modifier 时不会也没办法写横向的 Alignment,反之,也不会在 Column 中误写 Vertical 的 Alignment。

这其实大幅减少了 XML 中,无法轻易理解现在可以用什麽参数的问题。同样名称的 attribute 会出现在不同的 view 中,但是不同的 view 的同一个 attribute 却不一定能接受一样的参数。透过 Scope 可以简化整个流程,在防呆的同时也更轻易的引导开发者探索 API。

Unit

同样在 XML 中没有明确限制容易混淆的,是单位。dp, sp , em , px, pt ... 等等,不管是文字还是 View ,有太多单位需要了解。同时在 java 中,在设定数字时往往需要透过很多手段将简单的数字转成 dp 或 sp。

Compose 团队也用了很简单的方法解决了这个问题。

以最简单的文字为例:

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    fontSize: TextUnit = TextUnit.Unspecified,
    // .... 
) {
    // ....
}

Text 在接收 fontSize 的时候,接受的不是 sp 或 em,而是 TextUnit。TextUnit 是一个全新的东西,但是不用担心还要为了这个新的 type 特别转换,TextUnit 其实是 Compose 团队在 sp, em 这些可以用在文字的单位之上,抽象化的一个层级。

inline class TextUnitType(internal val type: Long) {
    companion object {
        val Unspecified = TextUnitType(UNIT_TYPE_UNSPECIFIED)
        val Sp = TextUnitType(UNIT_TYPE_SP)
        val Em = TextUnitType(UNIT_TYPE_EM)
    }
}

另外,在基本的数字型别上,也增加了将数字转成 TextUnit 的 extension function

val Float.sp: TextUnit get() = // ...
val Double.sp: TextUnit get() = // ...
val Int.sp: TextUnit get() = // ...

因此,在实际使用时就会变成这样。除了标记明确之外,也不会用错单位(例如在 Text 中用 dp )

Text(
    text = name,
    fontSize = 10.sp
)

用 inline class 表示单位

在研究过程中,发现 Compose team 还使用了另一个技巧:inline class。在上面的 TextUnitType 可以看得到,但以 Dp 举例更清楚。

inline class Dp(val value: Float)

inline class ( 或是 value class, inline 关键字未来会被 value 取代),是 kotlin 中乘载资料的一个方式。 inline class 可以接受一个(而且只能有一个 ) 参数,这个 inline class 就是这个参数的包装。举个例,当我今天要做一个 timer :

inline 关键字将会被 value 取代,详细原因可以参考KEEP 的文件。文章後续,依然会用inline class称呼,但是 code 会用 value 这个关键字。

value class Minutes(val minutes: Int)
value class Seconds(val seconds: Int)

class Timer {
    fun time(min: Int, sec: Int)            // fun 1
    fun time(min: Minutes, sec: Seconds)    // fun 2

    fun time(min: Minutes)                  // fun 3
    fun time(sec: Seconds)                  // fun 4
}

如果我们 Timer 的设计,如 fun 1 ,那我们在提供参数时有可能会误会 min 和 sec。透过 inline class 我们可以将 function 改写成 fun 2。这时我们要给两个的参数,即便两个的本质都是 Int,但在提供时可以透过 type 去限制也不会给错。同时,也因为两个参数的型别不同,也可以做到 fun 3fun 4 的 overload。

在 compile 後,inline class 会被转回所乘载资料的型别,也就是 Minutes 和 Seconds 在 compile 後都是以 Int 的方式在运作。

inline class 可以让某个值受到 type 的限制与保护,同时可以享受到 class 的能力 (ex.覆写 operator 等等),但同时又不会在 compile 後产生过多的负担。


今天没有特别讲到如何使用 compose 的其他 API,而是学习 Compose 团队如何设计 API。就如同他们在这个 talk里面提到的,使用 Compose 时每个人都是 library developer。学习如何优雅的设计 API,利用各种 Kotlin 提供优良且方便的语法与功能,在後续开发时可以写出更好的介面、更乾净的程序。


Reference:


<<:  学习Python纪录Day4 - Python资料型别

>>:  Day 4 基本型别 - part 1

DAY 3 『 RGB调色盘 - 每个物件的功能 』Part2

RGB调色盘:view + slider * 3 + textfield * 3 昨天介绍了如何拉物...

尚气与十环传奇

尚气与十环传奇在线观看 漫威影业荣誉出品史诗冒险《尚气与十环传奇》,结合前所未见的震撼性动作、令人惊...

30天学会C语言: Day 20-元元元运算子

一二三元 什麽叫做三元运算子?有三元运算子那有没有一元和二元运算子? 三元运算子就是运算元有三个的运...

第二十二天:为测试产生覆盖率报告

每当我们为专案写测试的时候,其实就是拿另一个程序来执行我们写的程序,看看是不是能将程序码里所有可能的...

第3章:基本存取命令列与终端机介绍

前言 从上一章节中,我们已经将实验与教学的环境给建置起来了,在这一章节中,将会演示使用「workst...