开始做 ImLog Android,要用 Kotlin,快速入门下,感觉有些东西挺有意思的。
变量声明:var 与 val
一开始觉得容易眼花—知道 val 是 value 后才恍然大悟。
?: Elvis Operator
这个奇怪的名字,原来是来自 Elvis Aaron Presley,上个世纪 icon 型的人物,他标志性的发型,从侧边看,就可这个符号一样。 哈哈,有点意思。
Unit
即是类型,也是值。多用在函数型参数定义的地方,表示 void 的含义。
尾随 Lambda
一开始接受不了,感觉把一个很规则化的东西搞得复杂了,但是看下 DS 给的例子,又觉得这东西确实挺精妙的。
// 测试框架(这看起来像英语)
describe("Calculator") {
it("should add correctly") {
expect(2 + 2).toBe(4)
}
}
// HTML 构建器(看起来像 HTML 本身)
html {
body {
h1 { +"Hello" }
p { +"World" }
}
}
// 如果用传统写法,这些 DSL 会非常丑陋
委托语法
入门 Jetpack Compose, 看到这个
var count by remember { mutableStateOf(0) }
by 将后面的 MutableState
而 { mutableStateOf(0) } 就是尾随 Lambda, remember 的第一个参数,省略了圆括号。
带接收者的函数类型 (Function literal with receiver )
fun buildString(action: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.action() // 在lambda内部,this指向sb
return sb.toString()
}
val html = buildString {
append("<html>")
append("</html>")
}
这段代码是天书,我真是看不懂。看了 DS 的解释,才大概知道。这里的 StringBuilder. 就是所谓的 receiver, 表示
- action 函数是在 StringBuilder 上执行的
- action 函数体内部,自动获得 this 指向 StringBuilder, 也就是内部也在 StringBuilder 作用域下
所以可以认为,这个 Function literal with receiver 是给框架作者用的!用户通过 lambda 尾随函数指定 receiver 要干的的事项,框架作者通过这个 Function 设置串联的逻辑。高阶函数。
最好理解等效的版本(函数版):
fun buildStringTraditional(action: (StringBuilder) -> Unit): String {
val sb = StringBuilder()
action(sb) // 要把 sb 作为参数传进去
return sb.toString()
}
val html2 = buildStringTraditional { sb ->
sb.append("<html>")
sb.append("</html>")
}
给类增加临时函数版:
fun StringBuilder.buildStringTemporaryMethod() {
append("<html>")
append("</html>")
}
val sb = StringBuilder()
sb.buildStringTemporaryMethod()
DSL
上面有点学习成本的语法,都属于是 DSL. 其实就是语言自己的一些语法糖或者设定。
还有个定义路由的例子:
// 没有 DSL(传统写法)
fun setupRoutes(router: Router) {
router.get("/users", handler::listUsers)
router.post("/users", handler::createUser)
router.get("/users/{id}", handler::getUser)
}
// 有 DSL(Ktor 框架实际用法)
routing {
get("/users") {
call.respond(listUsers())
}
post("/users") {
call.respond(createUser())
}
get("/users/{id}") {
call.respond(getUser(id))
}
}
- 传统代码:告诉计算机怎么做(创建对象、调用方法、转换类型)
- DSL 代码:告诉计算机做什么(构建字符串、定义路由、描述 UI)
Kotlin 文化
简洁优于显式: 代码要短,假设读者懂
Gradle 编译
上来就被编译给干懵了。
libs.versions.toml本质上就是一种官方推荐的“最佳实践和设计模式”,它在 Gradle 里被称作 Version Catalog(版本目录)。它和 Python 的 pyproject.toml 有着本质的区别。在 Gradle 中,这个 .toml 文件只是为了解决“当项目有几十个模块时,改一个版本号要改几十遍”的痛点而引入的一个全局变量本子。Gradle 只是在后台默默地帮你生成了一个名为 libs.versions.kotlin 的代码变量。这个变量现在静静地躺在内存里,如果你不在 build.gradle.kts 里去调用它,它对整个构建过程产生不了任何实质影响。 Gradle 只认 build.gradle.kts, libs.versions.toml 只是版本定义的一种最佳实践而已。
Kotlin 包可见性
同 package 内部是互相可见的,即使跨文件,也不需要 import.
package 是由文件头部的 package xxx 决定的,而非文件路径!!当然一般来说,对应起来最好。
类的定义
// 在这里就定义好构造函数的参数;
// 其中带 val/var 的,自动成为 class 的属性
// 不带的,就只是参数
class Person(val name: String, inputAge: Int) {
// 属性、方法默认都是 public
// 可用 private/protected/internal 来修饰
private val saveAge = inputAge - 1
// 可以定义延迟初始化的 var 属性;但访问未初始化的属性要崩溃;一般避免
lateinit var job: String
// 初始化方法
init {
println("Person ${name}")
}
// 公开方法;一般这种属性设置值只用用 setter
fun setJob(job: String) {
this.job = job;
}
}
太多的隐式规则了。
sealed class, 嵌套定义, class, data class, object, data object
sealed class, 密封类,类似增强版 Enum:可以听一不同类型的子类,然后用在 when 的地方,作用类似枚举。
可以嵌套定义,就是子类在父类的内部直接继承定义,也可以外部定义, 一般都是嵌套定义:
sealed class State {
object Idle : State()
data object Loading : State()
}
data class Success(val data: String): State()
class Error(val msg: String) : State()
object, 表示定义的是单例;
class 就是类型,有多实例。
data object 对 toString 等方法有优化,推荐用;
data class 表示是数据类,预定义好了 hash 等方法,类似 Python 里的 dataclass.
而基于 sealed class 下面可以有各种不同类型的子类,这就比普通 Enum 强太多了。
写到下面这个实际的 case,才一下子理解到 Sealed class + When 的优雅:
data class AppInitData(
val userId: UserId,
val storageUriSelected: Boolean,
val firstTopicCreated: Boolean,
val WelcomeShown: Boolean,
)
// 基于不同阶段,可以把后续阶段依赖的参数放进去;when 可以保证字段是符合预期的
private fun determineInitStep(initData: AppInitData?): AppInitStep = when {
initData == null -> AppInitStep.SignInUp
!initData.storageUriSelected -> AppInitStep.SelectMediaStorageUri(initData.userId)
!initData.WelcomeShown -> AppInitStep.Welcome(
userId = initData.userId, needCreateFirstTopic = !initData.firstTopicCreated
)
else -> AppInitStep.Finished
}
// 使用的时候,配合 when 也好做区分
when (val step = uiState.initStep) {
AppInitStep.Loading -> Splash()
AppInitStep.SignInUp -> SignInUpNavigation()
is AppInitStep.SelectMediaStorageUri -> SharedStorageSelectScreen(
currentUserId = step.userId,
)
...
}
如果不用这个,在外部结构里塞一个 UserId? 的类型,when 就爱莫能助,只能写 !! 这种吓人代码了。
实际遇到了,才明白它是真的优雅。
子类返回类型会自动 Import 进来
在继承关系中,子类不需要显式 import 父类中使用的类型。
背景:
@HiltWorker
class MediaFileProcessWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
private val fileManager: FileManager,
private val messageRepository: MessageRepository,
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {}
}
这里的 Result 并非 kotlin.Result, 而是 androidx.work.ListenableWorker.Result — 但它并没有通过 import 引入。
在我单独一个文件里,我没有 import , 则时用默认的 kotlin.Result. 还挺容易出错的…
经典问题:函数传参,为啥不能用 obj.fun
在 Kotlin 中,不带括号的 obj.fun 代表去获取一个属性—变量。而 fun 是函数,不是变量!
要引用函数,要用 ::,也就是 obj::fun.
所以,一般 2 种传参方式:
- lambda 包裹一层 【更推荐!扩展性好,容易理解;在 Jetpack Compose 1.5.3 之前版本里性能更稳定—自动 remember】
::引用
. 去引用函数,在 Kotlin 里是语法错误!