Appearance
Kotlin 编码规范
Kotlin 提供了大量语法糖,用得好能让代码简洁优雅,用得滥则适得其反。
本规范不是 Kotlin 语法教程,而是约定在 Android 项目中哪些特性该用、怎么用、哪些要避免。
如果你打算学习 Kotlin
理解 Kotlin 的关键,在于领会它将 面向对象 与 函数式编程 有机结合的设计哲学——这不是机械的拼凑,而是互为解药的化学反应:以面向对象稳固组件结构,以函数式编程净化数据逻辑。
如果你想系统入门或重塑心智:
变量声明
默认优先使用 val,只有变量在生命周期内确实需要重新赋值时,才使用 var。
- 大部分局部变量在创建后都不会再次赋值,直接使用
val更符合实际情况。 - 变量是否需要修改应该从声明处就能看出来。
- 减少无意义的可变状态,避免后续维护过程中出现意外重新赋值的问题。
推荐:
kotlin
// 集合引用不会改变,使用 val
val devices = repository.loadDevices()
val protocol = ProtocolType.MODBUS_RTU
// 需要重新赋值,使用 var
var retryCount = 0
while (retryCount < MAX_RETRY_COUNT) {
retryCount++
}避免:
kotlin
// 滥用 var,导致变量状态不可控
var devices = repository.loadDevices()
// ... 在后续冗长的业务逻辑中,极易被意外重新赋值
devices = emptyList() 空安全
优先非空设计
声明变量时,默认应设计为非空类型。只有当空值具有真实的业务含义时,才声明为可空类型。
kotlin
// 不要把所有变量都声明成可空的
var userName: String? = null
var deviceId: String? = null
// 非空是默认,可空是例外
var userName = ""
var deviceId = ""例外:
kotlin
// 设备未连接时确实为 null,可空合理
var currentDevice: Device? = null安全处理可空类型
安全调用与 Elvis 操作符
优先使用安全调用 ?. 和 Elvis 操作符 ?: 处理可空类型,避免使用 !! 强行断言非空。
正例:
使用 Kotlin 提供的空安全机制处理空值。
kotlin
// 安全调用
val length = text?.length
// Elvis:提供默认值
val name = user?.name ?: "未知用户"
// Elvis:提前返回
fun process(data: ByteArray?) {
val payload = data ?: return
parsePayload(payload)
}反例:
滥用 !!,主动放弃空安全保护。
kotlin
val name = user!!.profile!!.name!!使用原则
!!表示:"我保证这里不会为 null,如果为 null 就崩溃"- 如果连续出现多个
!!往往意味着设计存在问题
只有在明确保证非空时才用,并加上注释说明原因。
kotlin
// 此处 Fragment 由当前方法刚刚 add,必然存在
val fragment = supportFragmentManager.findFragmentByTag(TAG)!!数据类
纯数据载体
data class 用于纯数据载体,自动生成 equals()、hashCode()、toString()、copy(),适合作为数据模型。
kotlin
data class DeviceInfo(
val id: String,
val name: String,
val isOnline: Boolean,
val signalStrength: Int = 0
)data class 使用原则
- 只放数据字段,不写业务逻辑方法
- 字段尽量用
val(不可变),需要局部修改时用copy() - data class 默认是 final,不能被继承
- 通常也不应继承其他具有复杂状态和行为的父类
密封类与枚举
Kotlin 提供了 enum class 和 sealed class 两种有限状态建模方式。
固定常量集合
enum class 用于固定常量集合,当所有成员都是等价的无参常量时,使用枚举。
例如:
kotlin
/**
* 表示通信链路的固定状态
*/
enum class CommunicationState {
DISCONNECTED,
CONNECTING,
CONNECTED,
ERROR
}使用 when 时,编译器能够检查所有枚举值是否被覆盖。
kotlin
when (device.communicationState) {
CommunicationState.DISCONNECTED -> showOffline()
CommunicationState.CONNECTING -> showConnecting()
CommunicationState.CONNECTED -> showOnline()
CommunicationState.ERROR -> showError()
}状态建模
sealed class 用于状态建模,当不同状态需要携带不同数据时,使用密封类。
例如:
kotlin
/**
* 用于表示数据加载的状态
*
* @author 新一
* @date 2024/9/24 21:10
*/
sealed class Resource<out T> {
/**
* 表示正在加载状态
*/
data object Loading : Resource<Nothing>()
/**
* 表示加载成功,携带成功返回的值
*
* @param value 成功加载的数据
*/
data class Success<out T>(val value: T) : Resource<T>()
/**
* 表示加载失败,携带异常信息
*
* @param ex 加载过程中发生的异常
*/
data class Fail(val ex: Throwable) : Resource<Nothing>()
}不同状态可以拥有各自的数据结构。
kotlin
when (result) {
is Resource.Loading -> showLoading()
is Resource.Success -> {
showData(result.value)
}
is Resource.Fail -> {
showError(result.ex)
}
}使用建议
选择 enum class 还是 sealed class 的关键不在于状态数量,而在于状态是否需要携带不同数据。
- 固定常量集合:
enum class - 不同状态需要不同数据:
sealed class
优先让类型表达业务含义,而不是通过额外字段或魔法值区分状态。
扩展函数
虽然扩展函数能够优雅的为既有类扩展新能力,但由于它脱离了传统面向对象的定义与边界,所以极易演变为游离于架构之外的无序代码片段。
核心克制点
扩展函数应作为类的行为延伸,而非包罗万象的通用工具箱。
引入扩展函数的前提,是能让调用栈更加直观;而维持其可读性的关键,在于清晰的文件组织与严格的可见性边界。
组织方式
按扩展对象组织分类:
text
// View 相关扩展
extensions.ViewExtension.kt
// Context 相关扩展
extensions.ContextExtension.kt
// String 相关扩展
extensions.StringExtension.kt按业务模块组织分类:
text
// 设备模块相关扩展
DeviceExtension.kt
// 分析仪模块相关扩展
AnalyzerExtension.kt
// 协议模块相关扩展
ProtocolExtension.kt按功能领域组织分类:
text
// 数据存储相关扩展
DatabaseExtension.kt
// 网络通信相关扩展
NetworkExtension.kt
// 协程相关扩展
CoroutineExtension.kt实际项目中通常会组合使用多种组织方式,应根据扩展函数的职责和所属领域进行分类,重点是归属明确、便于查找。
避免无归属集中
避免将各种函数在一个文件里无序堆积。
例如:
kotlin
Extensions.kt
UtilsExtensions.kt
CommonExtensions.kt问题不在 “不能用”,而在于内容都在同一个文件里,扩展函数没有 “可定位性”。
可见性控制
放任默认的可见性,会导致局部的业务扩展无差别地入侵整个工程的自动补全列表,造成比较严重的语义污染。
可见性控制原则
- 如果扩展函数仅在当前模块内使用,必须追加
internal修饰符。 - 如果扩展函数仅在当前文件内使用,必须追加
private修饰符。 - 只有确信在全工程通用的核心底层工具属性,才允许保持默认的全局可见性。
反例:
缺少可见性约束,导致特定业务逻辑向全工程无序扩散。
kotlin
// 放在 CommonExtensions.kt 中,因缺少修饰符而暴露给全局
// 这会导致与该业务无关的 String 对象,也能在任意模块调用此方法
fun String.toDeviceProtocolType(): ProtocolType {
return ProtocolType.fromRawString(this)
}正例:
显式限制边界,只将能力赋予到需要它的地方。
kotlin
// 仅在设备模块内可见,切断跨模块的语义污染
internal fun String.toDeviceProtocolType(): ProtocolType {
return ProtocolType.fromRawString(this)
}
// 或在具体业务文件顶部声明为 private,仅供当前文件消费
// 仅用于当前界面的信号强度 UI 图标转换,不暴露给外部
private fun Int.toSignalIcon(): Int {
return when (this) {
in 0..30 -> R.drawable.ic_signal_weak
in 31..70 -> R.drawable.ic_signal_medium
else -> R.drawable.ic_signal_strong
}
}高阶函数
高阶函数(以函数作为参数或返回值的函数)是 Kotlin 极其强大的特性,但过度使用或嵌套过深会导致代码难以阅读。
作用域函数
作用域函数是 Kotlin 标准库提供的几个高阶函数,它们的唯一目的是在对象的上下文中执行代码块。
当对一个对象调用这些函数并提供一个 lambda 表达式时,会形成一个临时作用域。在该作用域内,可以访问该对象而无需其名称,因此这些函数称为作用域函数。
以下是 Kotlin 标准库中,最常用的五个作用域函数,它们各有适用场景,不要混用。
选择指南:
| 函数 | 接收者 | 返回值 | 典型用途 |
|---|---|---|---|
let | it | Lambda 结果 | 可空对象的非空处理,链式转换 |
run | this | Lambda 结果 | 对象初始化 + 计算结果 |
apply | this | 对象本身 | 对象属性配置(Builder 风格) |
also | it | 对象本身 | 副作用操作(日志、调试等) |
with | this | Lambda 结果 | 对非空对象执行多个操作(非链式场景) |
kotlin
// let:处理可空对象 / 链式转换
val nameLength = device.name?.let {
it.length
} ?: 0
// run:初始化 + 计算结果
val deviceInfo = device.run {
"$name-$id-$status"
}
// apply:配置对象属性,返回对象本身
val intent = Intent(context, MainActivity::class.java).apply {
putExtra("device_id", deviceId)
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
}
// also:执行副作用,不影响链式调用
val devices = repository.getDevices()
.also { Log.d(TAG, "Loaded ${it.size} devices") }
// with:对同一对象执行多个操作
with(binding) {
tvTitle.text = device.name
tvStatus.text = device.statusText
}使用技巧
先看是否可空,其次看是否需要返回值,最后再决定是否影响原对象。
警惕隐式 this 嵌套污染
严禁多层嵌套使用显式或隐式包含 this 的作用域函数(如 apply / run / with)。
当多个 this 在同一个代码块中交织时,上下文语义会被严重混淆。你以为你在配置子对象的属性,编译器可能正悄悄把它赋值给父对象,从而埋下极其隐蔽的 BUG。
反例:
kotlin
// 根本分不清哪个属性属于哪个对象,极易发生隐式赋值错误
mDialog.apply { // 这层的 this 是 Dialog
title = "设备配置"
binding.btnSubmit.run { // 这层的 this 是 Button
text = "激活设备"
isEnabled = isConnected // 惊悚:这里的 isConnected 到底是 Dialog 的、Button 的、还是外层 Activity 的?
}
} 正例:
若需嵌套,必须将其中一层拆解为具名参数 it,显式划清边界。
kotlin
// 边界清晰,语义各司其职
mDialog.apply { // this 是 Dialog
title = "设备配置"
// 使用 also/let 显式命名,将 Button 的上下文剥离出来
binding.btnSubmit.also { button ->
button.text = "激活设备"
button.isEnabled = this@MainActivity.isConnected // 显式指定外部 Activity 的状态
}
} Lambda
Lambda 是一把双刃剑,运用得当是提升代码表达力的利器;一旦过度嵌套或滥用,便会成为摧毁可读性的凶器。
总纲
Lambda 的核心克制点只有一个:保持单一语义边界
后面三条规则都是这个原则的具体情况。
参数语义
在多层 Lambda 嵌套或链式调用中,禁止继续使用默认参数名 it,也严禁使用 “编号式命名”(如 it1 / it2 / it3)。
反例一:
完全使用 it,语义丢失
it在嵌套作用域中无法区分指代对象- 没有可读性,需要不断回溯上下文才能判断当前层级含义
kotlin
orders.map {
it.items.map {
it.products.filter {
it.price > 100
}.map {
it.name
}
}
}反例二:
编号式命名,伪语义
it1 / it2 / it3仅表达“层级顺序”,不表达 “领域含义”- 没有维护性,一旦结构调整,编号体系立即失效,维护成本高
kotlin
orders.map { it1 ->
it1.items.map { it2 ->
it2.products.filter { it3 ->
it3.price > 100
}.map { it4 ->
it4.name
}
}
}此二反例,其本一致。
两个反例的问题一样:都没有表达业务语义,只是不同形式的匿名化。
正例:
使用具名语义参数
- 无需关注
Lambda的嵌套层级,仅通过参数名称即可理解当前处理的对象。
kotlin
orders.map { order ->
order.items.map { item ->
item.products
.filter { product ->
product.price > 100
}
.map { product ->
product.name
}
}
}链式复杂度
链式调用适用于表达 “单一数据流上的连续转换”,但当链路中出现多个语义阶段(过滤、展开、再过滤、再映射等),链式结构会掩盖中间语义,降低可读性与可维护性。
当无法用一个连续语义描述整个链路时,必须拆分为中间步骤或具名变量。
反例:
链式调用过长且语义混杂
- 隐式数据流难以拆解
- 调试也困难(无法单独观察中间状态)
- 修改某一阶段逻辑需要整体理解链路
kotlin
list.filter { it.isOnline }
.map { it.sensors }
.flatMap { it }
.filter { it.isActive }
.map { it.value }正例:
拆分为具名语义步骤,每个阶段仅负责一个明确的数据处理动作。
kotlin
val onlineDevices = list.filter { it.isOnline }
val sensors = onlineDevices.flatMap { device ->
device.sensors
}
val activeSensors = sensors.filter { sensor ->
sensor.isActive
}
val result = activeSensors.map { sensor ->
sensor.value
}例外情况
当链式调用语义单一且转换步骤不多时,可以保持链式写法,无需强制拆分。
kotlin
// 语义单一,只有 2 步,保持链式写法即可
val activeDeviceNames = devices.filter { it.isOnline }.map { it.name }拆分原则
- 当中间结果具备独立业务含义时,应拆分并命名,不要按步骤数量机械拆分
- 判断标准:能否为中间结果赋予有业务意义的变量名
嵌套控制
嵌套过深会导致逻辑层级不清晰,应将内层逻辑提取为具名函数。
以下是一个贴近真实 Repository 层的场景:
将 API 响应中的 DTO 映射为 UI 展示模型。
kotlin
// 嵌套过深,内层映射逻辑淹没在链式结构中,难以阅读
fun getDisplayItems(response: ApiResponse): List<DisplayItem> {
return response.data
.filter { it.isVisible }
.map { dto ->
dto.tags.filter { tag ->
tag.isEnabled
}.map { tag ->
DisplayItem(dto.id, tag.label)
}
}.flatten()
}
// 拆分后:职责分离,每一步意图一目了然
fun getDisplayItems(response: ApiResponse): List<DisplayItem> {
return response.data
.filter { it.isVisible } // 筛选可展示的数据
.flatMap { dto ->
// 将 DTO 转换为展示模型
toDisplayItems(dto)
}
}
private fun toDisplayItems(dto: DataDto): List<DisplayItem> {
return dto.tags
.filter { it.isEnabled } // 过滤可用标签
.map { tag ->
// 转换为展示模型
DisplayItem(dto.id, tag.label)
}
} 嵌套拆分原则
- 出现多层链式调用时,按数据处理阶段拆分为具名变量或函数
- 内层
Lambda包含独立映射逻辑时,必须提取为具名函数 - 判断标准:无法快速读懂内层意图时,就该提取
协程
协程是 Kotlin 推荐的异步编程方案。合理使用协程可以极大地简化异步逻辑,但如果不加限制地滥用,也容易导致内存泄漏和状态混乱。
谨慎使用 Global
GlobalScope 的生命周期与应用进程一致,不受 Activity、Fragment、ViewModel 等组件生命周期管理。
在 Android 开发中,大多数异步任务都应与特定生命周期绑定,避免任务失控、资源泄漏或无效状态更新。
反例:
使用 GlobalScope 执行界面相关任务
- 任务无法随页面或
ViewModel生命周期自动取消 - 容易导致内存泄漏或无效 UI 更新
kotlin
// Activity 销毁后,协程仍可能继续运行
GlobalScope.launch {
val data = repository.fetchData()
updateUI(data)
}正例:
使用与生命周期绑定的 Scope
kotlin
viewModelScope.launch {
val data = repository.fetchData()
updateUI(data)
}使用原则
- UI 相关任务使用
viewModelScope或lifecycleScope - 业务任务应绑定到明确的
CoroutineScope - 除应用级后台任务外,不应默认选择
GlobalScope
例外情况
当任务确实需要独立于页面、组件或业务生命周期长期存在时,可以使用 GlobalScope。
典型场景:
- 日志上报
- 崩溃信息收集
- 应用级监控任务
使用前应明确说明其脱离生命周期管理的原因。
结构化并发
协程应遵循结构化并发原则,确保任务生命周期清晰可控。
父协程会自动管理其所有子协程:
- 父协程结束前,会等待所有子协程执行完成
- 父协程取消时,所有子协程会一并取消
因此,不要在协程内部创建新的独立 CoroutineScope,否则会脱离父协程管理。
反例:
在协程内部创建独立作用域
- 这会破坏结构化并发,子任务不再受父协程控制
ViewModel销毁后任务仍可能继续执行,无端增加状态异常风险等隐性BUG
kotlin
viewModelScope.launch {
// 创建了新的协程作用域,切断与父协程的生命周期绑定
CoroutineScope(Dispatchers.IO).launch {
// 这里的任务不受 viewModelScope 控制,ViewModel 销毁后不会停止
repository.syncData()
}
}正例:
使用子协程继承父协程上下文
kotlin
viewModelScope.launch {
// 子协程自动继承 viewModelScope 生命周期
launch(Dispatchers.IO) {
repository.syncData()
}
}并行执行多个独立请求时,使用 async + awaitAll,创建结构化子协程:
kotlin
viewModelScope.launch {
// 并行请求设备数据和告警数据
val (devices, alerts) = awaitAll(
async { repository.fetchDevices() },
async { repository.fetchAlerts() }
)
// 所有任务完成后统一更新界面状态
_uiState.update {
it.copy(
devices = devices,
alerts = alerts
)
}
}使用原则
- 优先使用父协程提供的
launch、async等子协程 - 不要在协程内部随意创建新的
CoroutineScope - 子任务应始终纳入父协程生命周期管理
- 并发任务优先使用
async + awaitAll
异常处理
协程异常必须在协程作用域内处理,不要尝试在 launch 外层使用 try-catch 捕获协程内部异常。
反例:
在 launch 外层捕获异常
kotlin
try {
viewModelScope.launch {
repository.fetchDevices()
}
} catch (ex: Exception) {
// 无法捕获协程内部异常
}launch 是异步启动,协程异常发生时,早已脱离当前调用栈,因此外层 try-catch 无法捕获。
正例:
在协程内部处理异常
kotlin
viewModelScope.launch {
try {
val devices = repository.fetchDevices()
_uiState.update {
it.copy(devices = devices)
}
} catch (ex: CancellationException) {
// 协程取消属于正常控制流程
throw ex
} catch (ex: Exception) {
// 业务异常统一处理
_uiState.update {
it.copy(errorMessage = ex.message)
}
}
}补充说明
如果需要统一处理异常时,建议使用 CoroutineExceptionHandler
集合操作
Kotlin 标准库提供提供的集合 API 非常丰富,大多数情况下无需手写循环或复杂状态变量。
声明式操作优先
优先使用标准库表达数据处理意图,而不是手写循环。
反例:
手动维护结果集合
kotlin
val onlineDevices = mutableListOf<Device>()
for (device in devices) {
// 判断是否在线
if (device.isOnline) {
onlineDevices.add(device)
}
}正例:
kotlin
// 筛选在线设备
val onlineDevices = devices.filter { it.isOnline }常见操作:
kotlin
val onlineDevices = devices.filter { it.isOnline } // 过滤:只保留在线设备
val deviceNames = devices.map { it.name } // 转换:提取所有设备名称
val totalSignal = devices.sumOf { it.signalStrength } // 求和:计算信号强度总和
val hasOffline = devices.any { !it.isOnline } // 判断:是否存在离线设备
val allOnline = devices.all { it.isOnline } // 判断:是否全部在线
val grouped = devices.groupBy { it.type } // 分组:按设备类型分组不可变集合优先
对外暴露的集合属性应使用不可变类型,防止外部意外修改。
反例:
调用方可随意修改数据,会导致数据状态来源难以追踪
kotlin
// 对外直接暴露可变集合
val devices = mutableListOf<Device>()正例:
可变性应限制在最小作用域内,对外优先暴露不可变接口。
kotlin
private val _devices = mutableListOf<Device>()
val devices: List<Device>
get() = _devices // 对外仅暴露只读视图按需使用 Sequence
普通集合链式操作(如 filter 后接 map)时,会在每一步都创建一个新的临时列表。
当数据量较大或存在短路操作时,可使用 Sequence(序列)可以实现 惰性求值,元素会逐个穿过整个处理链,从而避免创建中间集合。此外,如果配合 take、first 等短路操作,Sequence 还能提前结束遍历,极大提升性能。
反例:
kotlin
val result = largeList
.filter { it.isOnline } // 遍历所有元素,创建中间集合 1
.map { it.toDisplayItem() } // 遍历集合 1,创建中间集合 2正例:
Sequence 惰性求值,不创建中间集合,内存更友好
kotlin
val result = largeList.asSequence()
.filter { it.isOnline }
.map { it.toDisplayItem() }
.toList() // 只有遇到终端操作(如 toList)时才开始真正执行短路场景:
如果存在短路操作(如 take(20)),Sequence 的优势会更明显。
kotlin
val result = largeList
.asSequence()
.filter { it.isOnline }
.map { it.toDisplayItem() }
// 获取前 20 条结果
.take(20)
.toList()找到 20 个满足条件的元素后就会立即停止,而不会处理集合内剩余的元素。
Sequence 的使用边界
数据量小(几百条以内)的场景不需要 Sequence,普通集合操作的开销可以忽略。过度使用 Sequence 反而增加代码复杂度。
Kotlin 的设计哲学

Kotlin 提供了丰富的语言特性,但这些特性的价值从来不在于减少代码量,而在于提升代码的表达能力。
Kotlin 鼓励简洁,但简洁不等于晦涩;Kotlin 追求表达力,但表达力不意味着炫技。
当某种写法让语法技巧盖过了业务语义时,即使代码看起来再 “高级”,也已经偏离了 Kotlin 的设计初衷。