消费者App kotlin实践
0、引子
kotlin 的介绍
https://www.kotlincn.net/
一、 Why kotlin?
- 完全兼容java
- 现代化的语法(集C++,java,ruby,python的优点)
- 简洁(很多的语法糖。有着java的性能和php的开发效率)
- Null safe
- 自动推导,模式匹配(类型自动推导,when 条件自动匹配)
- 支持Lambda,函数式编程(函数是第一公民)
- 支持扩展(方便 --无需继承或者装饰,可以为类、对象添加新方法、属性)
- 支持协程 (用同步的思维实现异步的功能,避免回调地狱)
现代化,高效的开发语言
二、 架构怎么选型?
“Architecture is About Intent, not Frameworks” - Robert C. Martin (Uncle Bob)
1.架构考虑的原则
软件有两个量(Value):
- 软件的功能或行为,即业务
- 软件的形状或设计,即架构
架构一句话概括:为了能够最小化创建和维护软件的成本。
基本原则:
- SOLID原则
- S:(Single Responsibility Principle) 单一职责原则
- O:(Open-Closed Principle)开闭原理
- L:(Liskov Substitution Principle)里氏替换原则
- I:(Interface Segregation Principle)接口隔离原理
- D:(Dependency Inversion Principle)依赖反转原理
- 分层原则
- 依赖原则
- 抽象原则
2.整洁架构(Clean architecture)
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
经典图
Android 项目中的进化图
https://fernandocejas.com/2018/05/07/architecting-android-reloaded/
核心思想:
- 一个核心
- 抽象性往内递增
- 外层依赖内层
- 层与层之间通过接口与适配器进行交流
- 可测试性
- 框架、数据库、UI 等都是细节
一般建议:
采用3层体系结构。好处:容易理解,非常熟悉。
三、UI层从MVP -> MVVM
1. 标准版MVP
- 优势:
- 增强应用的可测试性
- 更加干净的隔离视图和模型
- 劣势:
- 缺乏数据绑定支持
- present 持有view,model,随着功能复杂,会越来越庞大。
2.MVVM(databinding+liveData+ViewModel)
显著特点是利用了数据,视图绑定。简化代码相互的交互,分离view 和Model。
利用databinding 可以极大的简化View和data之间交互。
单向数据绑定
viewModel 数据触发xml视图更新
-双向数据绑定viewModel->xml视图更新
视图变化->修改viewModel数据源
优势:
简化代码,直接关注viewModel
自然的形成了数据,视图,视图数据的转换三个部分,从框架上提升了重用的粒度
也适用于RecyclerView,Adapter不再需要写更新视图的逻辑(通过独立绑定即可)
特别适合" 数据修改/设置页面",等数据UI交互、修改、保存 频繁的页面
劣势:
调试的难度变大,databinding语句异常不会有直接明显的报错,需要gradle build命令
databinding不支持kotlin拓展函数,语法表达能力很有限
一定程度降低了编译速度
视图事件监听处理方式依然不变
view层(xml)的响应可读性不强,后期维护成本会加大
3.消费者App 采用的方案?
改进版MVP(liveData+ViewModel)
- 优势:
- 立足于MVP进行,拥有V/M隔离等特性
- V 与 ViewModel 通过数据进行观察,减少V 与 ViewModel 的耦合
-劣势: - 简单UI会增加复杂度(可以酌情考虑)
MVP+(liveData+ViewModel)
数据视图分离,可测试性,简单高效开发,保持代码可维护性
四、kotlin实践点有哪些?
0.引入协程
协程是用于解决异步问题
有没有什么方法,即让代码串行编写,又让其执行并行的任务?
- Rxjava
-协程
Rxjava
随意看一段代码。
rxjava有以下特点:
- 1.代码调用比较复杂,穿插这各种线程切换,subscribe,observe
协程
Kotlin协程提供了一种简单的方案:async await方法。
//并发请求
GlobalScope.launch(Dispatchers.Main) {
//三次请求并发进行
valvalue1 = async { request1(parameter1) }
valvalue2 = async { request2(parameter2) }
valvalue3 = async { request3(parameter3) }
//所有结果全部返回后更新UI
updateUI(value1.await, value2.await, value3.await)
}
//requestAPI适配了Kotlin协程
suspend funrequest1(parameter : Parameter) {...}
suspend funrequest2(parameter : Parameter) {...}
suspend funrequest3(parameter : Parameter) {...}
.png)
//复杂业务逻辑的Kotlin协程实现
GlobalScope.launch(Dispatchers.Main) {
//首先拿到request1的请求结果
valvalue1 = request1(parameter1)
//将request1的请求结果用于request2和request3两个请求的并发进行
valvalue2 = async { request2(value1) }
valvalue3 = async { request2(value1) }
//用request2和request3两个请求结果更新UI
updateUI(value2.await, value3.await)
}
//requestAPI适配了Kotlin协程
suspend funrequest1(parameter : Parameter) {...}
suspend funrequest2(parameter : Parameter) {...}
suspend funrequest3(parameter : Parameter) {...}
协程的特点:
- 1.使用更轻松,可读性比较强
- 2.串行,尤其对异步请求之后的数据读取方便
- 3.支持协程连接等操作符,合并处理多个并发异步请求结果更方便
1.抛弃RxAndroid,切入协程
RxAndroid+Retrofit+OkHttp 是一个优秀的网络请求+数据切换组合。在kotlin的世界Retrofit+OKHttp+协程更香。
协程:更加轻量级的异步处理,用“同步”可读性编写异步功能的代码,减少回调操作,易懂。
切换后台环境执行
协程可以进行不同环境的切换,通常网络请求的发起为主线程,请求数据为后台,返回数据回到主线程。
协程的Dispatchers 提供4种切换:分别是 Dispatchers.Default,Dispatchers.Main,Dispatchers.Unconfined,Dispatchers.IO. 设置协程的环境为 Dispatchers.IO 即可切换至后台。
private suspend fun <T> withIO(
block: suspend CoroutineScope.() -> T): T {
return coroutineScope { withContext(Dispatchers.IO) { block.invoke(this) } }
}
数据异步返回
使用协程的扩展方法async 即可返回异步处理
suspend fun <T> CoroutineScope.optAsync(coroutineContext: CoroutineContext,
block: suspend CoroutineScope.() -> T): Deferred<T?> {
return coroutineScope {
async(coroutineContext) {
block.invoke(this@optAsync)
}
}
}
Retrofit 转化协程
Retrofit 在 2.6.0 之后支持suspend 类型。
如下
// homepage
@Convert(Converters.APP)
@Headers(Host.APP_CONVERT , Header.AUTH)
@GET("v2/home/banner")
suspend fun getHomeBanner(@QueryMap map : Map<String , String>) : List<BannerType.ItemsBean>
2.ViewModel 更新数据,绑定生命周期
在viewModel中对每一个model 关联一个liveData。数据的更新,修改直接触发liveData的变化。从而达到在View层订阅数据的UI发生改变。
3. 使用函数式编程思想
kotlin支持传统的面向对象编程+函数式编程两种方式。
y=f(x)一种输入,输出的映射关系
将程序描述为表达式和变换,以数学的方式建立模型,尽量避免可变的状态.用高阶抽象取代基本的控制结构
函数式思维转变
核心:抽象
目的:
从细节中解脱出来,解答问题中非重复部分
-
对于通用的集合,数组的操作可以首选函数式变换 例如filter,map,fold,reduce等来代替各种迭代、遍历、过滤、筛选操作。
- 使用函数进行方便设计一些设计模式
函数作为参数是高阶函数,而函数又可以有高度的抽象,约定输入(参数)和输出(返回值)即可。对于函数内部的实现可以忽略,一个函数也没有像java中对象必须属于某一个类型的约束,因此极具抽象性。可以利用这一点进行简化一些设计模式,例如模板方法,策略模式,简化工场模式。
- 使用Either 类型返回两种不同类型的数据
Either 是函数式思想中的一种类型返回 两种不同类型的数据。适合在网络请求返回结果中使用.Either<Exception,Result> 当结果正常的时候返回 右值->Result ,当出现异常的时候返回左值Exception。从而避免 java中的return null导致的NPE.
kotlin 本身没有这个函数,需要自己写一个,或者引入外部库
sealed class Either<out L, out R> {
/** * Represents the left side of [Either] class which by convention is a "Failure". */
data class Left<out L>(val a: L) : Either<L, Nothing>()
/** * Represents the right side of [Either] class which by convention is a "Success". */
data class Right<out R>(val b: R) : Either<Nothing, R>()
...
4.善用扩展方法
Kotlin或框架提供了很多方便使用的拓展方法,可以避免我们写很多模板代码,主要体现在基本数据类型和集合上.
视图
// 视图可视状态的拓展
imageView.visibleOrGone(null != personalInfo)
imageView.visibleOrInVisible(null != personalInfo)
imageView.visible()
imageView.inVisible()
imageView.gone()
imageView.isVisible()
// ImageView的拓展
// 加载图片,圆角为5dp
imageView.cornerize(context, url, 5.dp)
// 加载圆形图片
imageView.circlize(context, url)
// 加载图片
ImageView.load(context, url)
常用数据类型
// 字符串
// 和Java不同,使用 == 而不需要用 TextUtils.equals,因为Kotlin重载了 == 操作符
TextUtils.equals(s1, s2)
s1 == s2
// isNullOrEmpty是空也可以调用的拓展方法,详细见源码;使用他代替下面写法
s == null || s.length() == 0
s.isNullOrEmpty
// orEmpty拓展方法
val result = if(s == null) "" else s
val result1 = s.orEmpty()
// isNotEmpty拓展
if(s.length != 0)
if(s.isNotEmpty())
// 字符串转整形或浮点型不再需要try catch了
// 使用toIntOrNull,如果转换异常返回空
val number: Int? = str.toIntOrNull()
val doubleNum: Double? = str.toDoubleOrNull
集合扩展
// 集合
// 从一个集合中找到一个元素并用他做一些事情
PersonalMenu.values().find { it.ordinal == requestCode }?.let { jumpToMenu(it) }
// 遍历
creators.forEach { creator -> }
creators.forEachIndexed { index, creator -> }
// 需要注意的是forEach这类的拓展函数,不支持cotinue和break,continue可以用return @forEach 来代替
// 拓展一下,如果需要跳出代码块也可以用return @myBlock的方式,但是需要声明代码块的名字,实例如下
@myBlock {
// do something
if(a) {
return @myBlock
}
}
startActivity扩展
val intent = Intent(this, IdCardResultActivity::class.java)
intent.putExtra(IdCardResultActivity.NAME, it.name)
intent.putExtra(IdCardResultActivity.NUMBER, it.number)
startActivity(intent)
// 可以简化为
startActivity<IdCardResultActivity>(
IdCardResultActivity.NAME to it.name,
IdCardResultActivity.NUMBER to it.number)
5.使用常用操作符
- ?. 进行非空操作,代码优雅
val dummyContext = this.context
if(dummyContext != null) {
dummyContext.startActivity(intent)
}
// 改进这样
context?.let{ it.startActivity(intent) }
- apply (构建参数,build,并返回)
// 如果频频对一个东西设置参数可以用apply
mediaPlayer.prepare()
mediaPlayer.setVolume(1.0f, 1.0f)
mediaPlayer.setSomeParam(2)
mediaPlayer.setAnotherParam(PARAM)
mediaPlayer.setOtherParams("", "")
// 可以用apply操作符改善
mediaPlayer.prepare().apply{
setVolume(1.0f, 1.0f)
someParam = 2
anotherParam = PARAM
setOtherParams("", "")
}
- with (对同一对象设置不同的属性)
// 例如在adapter中绑定数据的过程中,频繁访问数据字段
holder.desc.text = entity.desc
holder.remark.text = entity.remark
holder.phone.text = entity.remark
holder.sex.text = entity.remark
// 可以改善为
with(entity) {
holder.desc.text = desc
holder.remark.text = remark
holder.phone.text = phone
holder.sex.text = sex
}
- run
// 节省了对实体的点操作
// 可以看到上面例子中with不是一个拓展,而是一个全局方法
// with包裹的对象是不具备判空的能力的,因此with执行块中,需要对with对象判空
if(entity != null) {
with(entity) {
...
}
}
// 如果希望省去这个步骤,则可以使用run
// run 既是一个拓展,又可以让执行块切换this的上下文,不用再写it点操作
entity?.run{
holder.desc.text = desc
holder.remark.text = remark
holder.phone.text = phone
holder.sex.text = sex
}
6. by 委托作用大
by关键字主要有两种用途,一种是接口代理,另一种是属性代理。
- 接口代理 (代理模式)
package delegate
interface Api {
fun eat()
fun play()
}
class ApiImpl(api: Api) : Api by api
- 属性代理
kotlin中属性的get和set方法的代理。属性代理不需要实现任何方法,但是他们得提供一个getValue方法(如果是var,还得提供一个setValue方法)。
package delegate
import kotlin.reflect.KProperty
class M {
val s: String by MyDelegate { "Hello" }
}
class MyDelegate<T>(val init: () -> T) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return init()
}
}
鉴于by的强大可以在项目中使用。kotlin中提供 lazy (延迟加载的委托)比较实用。
对于sp很实用(代理getter,setter)
7. LiveDataBus 代替 Rxbus
LiveData是一个可以被观察的数据持有类,他可以感知并遵循Activity,Fragmet或者Service等组件的生命周期。
livedata的优点:
- 采用观察者模式,UI和实时数据保持一致
- 绑定生命周期,避免内存泄漏,
- 不会再产生由于Activity处于stop状态而引起的崩溃
- 不需要再解决生命周期带来的问题,LiveData可以感知被绑定的组件的生命周期,只有在活跃状态才会通知数据变化
- 实时数据刷新,当组件处于活跃状态或者从不活跃状态到活跃状态时总是能收到最新的数据。
可采用github开源 https://github.com/JeremyLiao/LiveEventBus
也可以自己封装。
val wxLogin = BusLiveData<String>()
wxLogin.observe(this, Observer {
//todo 业务代码
})
8.使用Anko简化和方便Koltin的开发
https://github.com/Kotlin/anko
-
Anko Commons:
一个轻量级的工具集 包括但不仅限 intents, dialogs toast, logging ,Resources 和 dimensions;
最显著的一个特点:比knifeButter 还简单,不需要findById,不需要注解。直接在View中使用控件Id即可拿到控件实例。
例如:
<Button
android:id="@+id/btn_enter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
btn_enter.setOnClickListener {
startActivity<MainActivity>()
overridePendingTransition(0, 0)
finish()
}
-
Anko Layouts:
可以使用DSL(Domain Specific Language)动态创建我们的UI.类似flutter,swiftUI
声明式布局,目前jetpack 采用compose进行增强版,支持界面预览功能。
private fun showCustomerLayout() {
verticalLayout {
padding = dip(30)
editText {
hint = "Name"
textSize = 24f
}.textChangedListener {
onTextChanged { str, _, _, _ ->
println(str)
}
}
editText {
hint = "Password"
textSize = 24f
}.textChangedListener {
onTextChanged { str, _, _, _ ->
println(str)
}
}
button("跳转到其它界面") {
textSize = 26f
id = BTN_ID
onClick {
// 界面跳转并携带参数
startActivity<IntentActivity>("name" to "小明", "age" to 12)
}
}
}
-
Anko SQLite:
简化SQL的繁琐操作,简化游标查询操作,执行完操作后进行自动关闭资源。
database.use {
// 在这里可以写所有的增,删,改,查的操作
//完成后会自动会关闭资源
}
class SomeActivity : Activity() {
private fun loadAsync() {
async(UI) {
val result = bg {
database.use { ... }
}
loadComplete(result)
}
}
}
五、Kotlin使用的一些坑
- 包大小变大了
- 构建时间会增加一点点
- 在使用data 数据作为bean的时候需要注意 参数是否可空。(如果是不可为空的参数,在gson解析遇到空字段会引发NPE)
- Koltin 中相关函数分布会比较发散,需要合理的管理。(可以支持顶层函数,扩展函数等灵活语法)
六、kotlin编码规范
本页包含当前 Kotlin 语言的编码风格。
应用风格指南
如需根据本风格指南配置 IntelliJ 格式化程序,请安装 Kotlin 插件
1.2.20 或更高版本,转到 Settings | Editor | Code Style | Kotlin,点击右
上角的 Set from... 链接,并从菜单中选择 Predefined style | Kotlin style guide。
如需验证代码已按风格指南格式化,请转到探查设置(Inspections)并启用
Kotlin | Style issues | File is not formatted according to project settings 探查项。
验证风格指南中描述的其他问题(如命名约定)的附加探查项默认已启用。
源代码组织
目录结构
在纯 Kotlin 项目中,推荐的目录结构遵循
省略了公共根包的包结构。例如,如果项目中的所有代码都位于 org.example.kotlin
包及其
子包中,那么 org.example.kotlin
包的文件应该直接放在源代码根目录下,而
org.example.kotlin.network.socket
中的文件应该放在源代码根目录下的 network/socket
子目录中。
对于 JVM 平台:Kotlin 源文件应当与 Java 源文件位于同一源文件根目录下,
并遵循相同的目录结构(每个文件应存储在与其 package 语句对应的目录中
。
源文件名称
如果 Kotlin 文件包含单个类(以及可能相关的顶层声明),那么文件名应该与
该类的名称相同,并追加 .kt 扩展名。如果文件包含多个类或只包含顶层声明,
那么选择一个描述该文件所包含内容的名称,并以此命名该文件。
使用首字母大写的驼峰风格(例如 ProcessDeclarations.kt
)。
文件的名称应该描述文件中代码的作用。因此,应避免在文件名中使用
诸如“Util”之类的无意义词语。
源文件组织
鼓励多个声明(类、顶级函数或者属性)放在同一个 Kotlin 源文件中,
只要这些声明在语义上彼此紧密关联并且文件保持合理大小(不超过4-5百行)。
特别是在为类定义与类的所有客户都相关的扩展函数时,
请将它们放在与类自身定义相同的地方。而在定义仅对指定客户有意义的扩展函数时,请将它们放在紧挨该客户代码之后。不要只是为了保存“Foo 的所有扩展函数”而创建文件。
类布局
通常,一个类的内容按以下顺序排列:
- 属性声明与初始化块
- 次构造函数
- 方法声明
- 伴生对象
不要按字母顺序或者可见性对方法声明排序,也不要将常规方法与扩展方法分开。而是要把相关的东西放在一起,这样从上到下阅读类的人就能够跟进所发生事情的逻辑。选择一个顺序(高级别优先,或者相反)并坚持下去。
将嵌套类放在紧挨使用这些类的代码之后。如果打算在外部使用嵌套类,而且类中并没有引用这些类,那么把它们放到末尾,在伴生对象之后。
接口实现布局
在实现一个接口时,实现成员的顺序应该与该接口的成员顺序相同(如果需要,还要插入用于实现的额外的私有方法)
重载布局
在类中总是将重载放在一起。
命名规则
在 Kotlin 中,包名与类名的命名规则非常简单:
包的名称总是小写且不使用下划线(
org.example.project
)。
通常不鼓励使用多个词的名称,但是如果确实需要使用多个词,可以将它们连接在一起
或使用驼峰风格(org.example.myProject
)。类与对象的名称以大写字母开头并使用驼峰风格:
open class DeclarationProcessor { /*……*/ }
object EmptyDeclarationProcessor : DeclarationProcessor() { /*……*/ }
函数名
函数、属性与局部变量的名称以小写字母开头、使用驼峰风格而不使用下划线:
fun processDeclarations() { /*……*/ }
var declarationCount = 1
例外:用于创建类实例的工厂函数可以与要创建的类具有相同的名称:
abstract class Foo { /*……*/ }
class FooImpl : Foo { /*……*/ }
fun FooImpl(): Foo { return FooImpl() }
测试方法的名称
当且仅当在测试中,可以使用反引号括起来的带空格的方法名。
(请注意,Android 运行时目前不支持这样的方法名。)测试代码中也允许方法名使用下划线。
class MyTestCase {
@Test fun `ensure everything works`() { /*...*/ }
@Test fun ensureEverythingWorks_onAndroid() { /*...*/ }
}
属性名
常量名称(标有 const
的属性,或者保存不可变数据的没有自定义 get
函数的顶层/对象 val
属性)应该使用大写、下划线分隔的名称:
const val MAX_COUNT = 8
val USER_NAME_FIELD = "UserName"
保存带有行为的对象或者可变数据的顶层/对象属性的名称应该使用驼峰风格名称:
val mutableCollection: MutableSet<String> = HashSet()
保存单例对象引用的属性的名称可以使用与 object
声明相同的命名风格:
val PersonComparator: Comparator<Person> = /*...*/
对于枚举常量,可以使用大写、下划线分隔的名称
(enum class Color { RED, GREEN }
)也可使用首字母大写的常规驼峰名称,具体取决于用途。
幕后属性的名称
如果一个类有两个概念上相同的属性,一个是公共 API 的一部分,另一个是实现细节,那么使用下划线作为私有属性名称的前缀:
class C {
private val _elementList = mutableListOf<Element>()
val elementList: List<Element>
get() = _elementList
}
选择好名称
类的名称通常是用来解释类是什么的名词或者名词短语:List
、 PersonReader
。
方法的名称通常是动词或动词短语,说明该方法做什么:close
、 readPersons
。
修改对象或者返回一个新对象的名称也应遵循建议。例如 sort
是对一个集合就地排序,而 sorted
是返回一个排序后的集合副本。
名称应该表明实体的目的是什么,所以最好避免在名称中使用无意义的单词
(Manager
、 Wrapper
等)。
当使用首字母缩写作为名称的一部分时,如果缩写由两个字母组成,就将其大写(IOStream
);
而如果缩写更长一些,就只大写其首字母(XmlFormatter
、 HttpInputStream
)。
格式化
使用 4 个空格缩进。不要使用 tab。
对于花括号,将左花括号放在结构起始处的行尾,而将右花括号放在与左括结构横向对齐的单独一行。
if (elements != null) {
for (element in elements) {
// ……
}
}
(注意:在 Kotlin 中,分号是可选的,因此换行很重要。语言设计采用
Java 风格的花括号格式,如果尝试使用不同的格式化风格,那么可能会遇到意外的行为。)
横向不空白
在二元操作符左右留空格(a + b
)。例外:不要在“range to”操作符(0..i
)左右留空格。
不要在一元运算符左右留空格(a++
)
在控制流关键字(if
、 when
、 for
以及 while
)与相应的左括号之间留空格。
不要在主构造函数声明、方法声明或者方法调用的左括号之前留空格。
class A(val x: Int)
fun foo(x: Int) { …… }
fun bar() {
foo(1)
}
绝不在 (
、 [
之后或者 ]
、 )
之前留空格。
绝不在.
或者 ?.
左右留空格:foo.bar().filter { it > 2 }.joinToString()
, foo?.bar()
在 //
之后留一个空格:// 这是一条注释
不要在用于指定类型参数的尖括号前后留空格:class Map<K, V> { …… }
不要在 ::
前后留空格:Foo::class
、 String::length
不要在用于标记可空类型的 ?
前留空格:String?
作为一般规则,避免任何类型的水平对齐。将标识符重命名为不同长度的名称不应该影响声明或者任何用法的格式。
冒号
在以下场景中的 :
之前留一个空格:
- 当它用于分隔类型与超类型时;
- 当委托给一个超类的构造函数或者同一类的另一个构造函数时;
- 在
object
关键字之后。
而当分隔声明与其类型时,不要在 :
之前留空格。
在 :
之后总要留一个空格。
abstract class Foo<out T : Any> : IFoo {
abstract fun foo(a: Int): T
}
class FooImpl : Foo() {
constructor(x: String) : this(x) { /*……*/ }
val x = object : IFoo { /*……*/ }
}
类头格式化
具有少数主构造函数参数的类可以写成一行:
class Person(id: Int, name: String)
具有较长类头的类应该格式化,以使每个主构造函数参数都在带有缩进的独立的行中。
另外,右括号应该位于一个新行上。如果使用了继承,那么超类的构造函数调用或者所实现接口的列表应该与右括号位于同一行:
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name) { /*……*/ }
对于多个接口,应该将超类构造函数调用放在首位,然后将每个接口应放在不同的行中:
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name),
KotlinMaker { /*……*/ }
对于具有很长超类型列表的类,在冒号后面换行,并横向对齐所有超类型名:
class MyFavouriteVeryLongClassHolder :
MyLongHolder<MyFavouriteVeryLongClass>(),
SomeOtherInterface,
AndAnotherOne {
fun foo() { /*...*/ }
}
为了将类头与类体分隔清楚,当类头很长时,可以在类头后放一空行
(如上例所示)或者将左花括号放在独立行上:
class MyFavouriteVeryLongClassHolder :
MyLongHolder<MyFavouriteVeryLongClass>(),
SomeOtherInterface,
AndAnotherOne
{
fun foo() { /*...*/ }
}
构造函数参数使用常规缩进(4 个空格)。
理由:这确保了在主构造函数中声明的属性与
在类体中声明的属性具有相同的缩进。
修饰符
如果一个声明有多个修饰符,请始终按照以下顺序安放:
public / protected / private / internal
expect / actual
final / open / abstract / sealed / const
external
override
lateinit
tailrec
vararg
suspend
inner
enum / annotation
companion
inline
infix
operator
data
将所有注解放在修饰符前:
@Named("Foo")
private val foo: Foo
除非你在编写库,否则请省略多余的修饰符(例如 public
)。
注解格式化
注解通常放在单独的行上,在它们所依附的声明之前,并使用相同的缩进:
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude
无参数的注解可以放在同一行:
@JsonExclude @JvmField
var x: String
无参数的单个注解可以与相应的声明放在同一行:
@Test fun foo() { /*……*/ }
文件注解
文件注解位于文件注释(如果有的话)之后、package
语句之前,并且用一个空白行与 package
分开(为了强调其针对文件而不是包)。
/** 授权许可、版权以及任何其他内容 */
@file:JvmName("FooBar")
package foo.bar
函数格式化
如果函数签名不适合单行,请使用以下语法:
fun longMethodName(
argument: ArgumentType = defaultValue,
argument2: AnotherArgumentType
): ReturnType {
// 函数体
}
函数参数使用常规缩进(4 个空格)。
理由:与构造函数参数一致
对于由单个表达式构成的函数体,优先使用表达式形式。
fun foo(): Int { // 不良
return 1
}
fun foo() = 1 // 良好
表达式函数体格式化
如果函数的表达式函数体与函数声明不适合放在同一行,那么将 =
留在第一行。
将表达式函数体缩进 4 个空格。
fun f(x: String) =
x.length
属性格式化
对于非常简单的只读属性,请考虑单行格式:
val isEmpty: Boolean get() = size == 0
对于更复杂的属性,总是将 get
与 set
关键字放在不同的行上:
val foo: String
get() { /*……*/ }
对于具有初始化器的属性,如果初始化器很长,那么在等号后增加一个换行
并将初始化器缩进四个空格:
private val defaultCharset: Charset? =
EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)
格式化控制流语句
如果 if
或 when
语句的条件有多行,那么在语句体外边总是使用大括号。
将该条件的每个后续行相对于条件语句起始处缩进 4 个空格。
将该条件的右圆括号与左花括号放在单独一行:
if (!component.isSyncing &&
!hasAnyKotlinRuntimeInScope(module)
) {
return createKotlinNotConfiguredPanel(module)
}
理由:对齐整齐并且将条件与语句体分隔清楚
将 else
、 catch
、 finally
关键字以及 do/while 循环的 while
关键字与
之前的花括号放在相同的行上:
if (condition) {
// 主体
} else {
// else 部分
}
try {
// 主体
} finally {
// 清理
}
在 when
语句中,如果一个分支不止一行,可以考虑用空行将其与相邻的分支块分开:
private fun parsePropertyValue(propName: String, token: Token) {
when (token) {
is Token.ValueToken ->
callback.visitValue(propName, token.value)
Token.LBRACE -> { // ……
}
}
}
将短分支放在与条件相同的行上,无需花括号。
when (foo) {
true -> bar() // 良好
false -> { baz() } // 不良
}
方法调用格式化
在较长参数列表的左括号后添加一个换行符。按 4 个空格缩进参数。
将密切相关的多个参数分在同一行。
drawSquare(
x = 10, y = 10,
width = 100, height = 100,
fill = true
)
在分隔参数名与值的 =
左右留空格。
链式调用换行
当对链式调用换行时,将 .
字符或者 ?.
操作符放在下一行,并带有单倍缩进:
val anchor = owner
?.firstChild!!
.siblings(forward = true)
.dropWhile { it is PsiComment || it is PsiWhiteSpace }
调用链的第一个调用通常在换行之前,当然如果能让代码更有意义也可以忽略这点。
Lambda 表达式格式化
在 lambda 表达式中,应该在花括号左右以及分隔参数与代码体的箭头左右留空格。
如果一个调用接受单个 lambda 表达式,应该尽可能将其放在圆括号外边传入。
list.filter { it > 10 }
如果为 lambda 表达式分配一个标签,那么不要在该标签与左花括号之间留空格:
fun foo() {
ints.forEach lit@{
// ……
}
}
在多行的 lambda 表达式中声明参数名时,将参数名放在第一行,后跟箭头与换行符:
appendCommaSeparated(properties) { prop ->
val propertyValue = prop.get(obj) // ……
}
如果参数列表太长而无法放在一行上,请将箭头放在单独一行:
foo {
context: Context,
environment: Env
->
context.configureEnv(environment)
}
文档注释
对于较长的文档注释,将开头 /**
放在一个独立行中,并且后续每行都
以星号开头:
/**
* 这是一条多行
* 文档注释。
*/
简短注释可以放在一行内:
/** 这是一条简短文档注释。 */
通常,避免使用 @param
与 @return
标记。而是将参数与返回值的描述
直接合并到文档注释中,并在提到参数的任何地方加上参数链接。
只有当需要不适合放进主文本流程的冗长描述时才应使用 @param
与 @return
。
// 避免这样:
/**
* Returns the absolute value of the given number.
* @param number The number to return the absolute value for.
* @return The absolute value.
*/
fun abs(number: Int) { /*……*/ }
// 而要这样:
/**
* Returns the absolute value of the given [number].
*/
fun abs(number: Int) { /*……*/ }
避免重复结构
一般来说,如果 Kotlin 中的某种语法结构是可选的并且被 IDE
高亮为冗余的,那么应该在代码中省略之。为了清楚起见,不要在代码中保留不必要的语法元素
。
Unit
如果函数返回 Unit,那么应该省略返回类型:
fun foo() { // 这里省略了“: Unit”
}
分号
尽可能省略分号。
字符串模版
将简单变量传入到字符串模版中时不要使用花括号。只有用到更长表达式时才使用花括号。
println("$name has ${children.size} children")
语言特性的惯用法
不可变性
优先使用不可变(而不是可变)数据。初始化后未修改的局部变量与属性,总是将其声明为 val
而不是 var
。
总是使用不可变集合接口(Collection
, List
, Set
, Map
)来声明无需
改变的集合。使用工厂函数创建集合实例时,尽可能选用返回不可变
集合类型的函数:
// 不良:使用可变集合类型作为无需改变的值
fun validateValue(actualValue: String, allowedValues: HashSet<String>) { …… }
// 良好:使用不可变集合类型
fun validateValue(actualValue: String, allowedValues: Set<String>) { …… }
// 不良:arrayListOf() 返回 ArrayList<T>,这是一个可变集合类型
val allowedValues = arrayListOf("a", "b", "c")
// 良好:listOf() 返回 List<T>
val allowedValues = listOf("a", "b", "c")
默认参数值
优先声明带有默认参数的函数而不是声明重载函数。
// 不良
fun foo() = foo("a")
fun foo(a: String) { /*……*/ }
// 良好
fun foo(a: String = "a") { /*……*/ }
类型别名
如果有一个在代码库中多次用到的函数类型或者带有类型参数的类型,那么最好为它定义
一个类型别名:
typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<String, Person>
Lambda 表达式参数
在简短、非嵌套的 lambda 表达式中建议使用 it
用法而不是
显式声明参数。而在有参数的嵌套 lambda 表达式中,始终应该显式声明参数。
在 lambda 表达式中返回
避免在 lambda 表达式中使用多个返回到标签。请考虑重新组织这样的 lambda 表达式使其只有单一退出点。
如果这无法做到或者不够清晰,请考虑将 lambda 表达式转换为匿名函数。
不要在 lambda 表达式的最后一条语句中使用返回到标签。
命名参数
当一个方法接受多个相同的原生类型参数或者多个 Boolean
类型参数时,请使用命名参数语法,
除非在上下文中的所有参数的含义都已绝对清楚。
drawSquare(x = 10, y = 10, width = 100, height = 100, fill = true)
使用条件语句
优先使用 try
、if
与 when
的表达形式。例如:
return if (x) foo() else bar()
return when(x) {
0 -> "zero"
else -> "nonzero"
}
优先选用上述代码而不是:
if (x)
return foo()
else
return bar()
when(x) {
0 -> return "zero"
else -> return "nonzero"
}
if
还是 when
二元条件优先使用 if
而不是 when
。不要使用
when (x) {
null -> // ……
else -> // ……
}
而应使用 if (x == null) …… else ……
如果有三个或多个选项时优先使用 when
。
在条件中使用可空的 Boolean
值
如果需要在条件语句中用到可空的 Boolean
, 使用 if (value == true)
或 if (value == false)
检测。
使用循环
优先使用高阶函数(filter
、map
等)而不是循环。例外:forEach
(优先使用常规的 for
循环,
除非 forEach
的接收者是可空的或者 forEach
用做长调用链的一部分。)
当在使用多个高阶函数的复杂表达式与循环之间进行选择时,请了解
每种情况下所执行操作的开销并且记得考虑性能因素。
区间上循环
使用 until
函数在一个开区间上循环:
for (i in 0..n - 1) { /*……*/ } // 不良
for (i in 0 until n) { /*……*/ } // 良好
使用字符串
优先使用字符串模板而不是字符串拼接。
优先使用多行字符串而不是将 \n
转义序列嵌入到常规字符串字面值中。
如需在多行字符串中维护缩进,当生成的字符串不需要任何内部
缩进时使用 trimIndent
,而需要内部缩进时使用 trimMargin
:
assertEquals(
"""
Foo
Bar
""".trimIndent(),
value
)
val a = """if(a > 1) {
| return a
|}""".trimMargin()
函数还是属性
在某些情况下,不带参数的函数可与只读属性互换。
虽然语义相似,但是在某种程度上有一些风格上的约定。
底层算法优先使用属性而不是函数:
- 不会抛异常
- 计算开销小(或者在首次运行时缓存)
- 如果对象状态没有改变,那么多次调用都会返回相同结果
使用扩展函数
放手去用扩展函数。每当你有一个主要用于某个对象的函数时,可以考虑使其成为
一个以该对象为接收者的扩展函数。为了尽量减少 API 污染,尽可能地限制
扩展函数的可见性。根据需要,使用局部扩展函数、成员扩展函数
或者具有私有可视性的顶层扩展函数。
使用中缀函数
一个函数只有用于两个角色类似的对象时才将其声明为中缀函数。良好示例如:and
、 to
、zip
。
不良示例如:add
。
如果一个方法会改动其接收者,那么不要声明为中缀形式。
工厂函数
如果为一个类声明一个工厂函数,那么不要让它与类自身同名。优先使用独特的名称,
该名称能表明为何该工厂函数的行为与众不同。只有当确实没有特殊的语义时,
才可以使用与该类相同的名称。
例如:
class Point(val x: Double, val y: Double) {
companion object {
fun fromPolar(angle: Double, radius: Double) = Point(...)
}
}
如果一个对象有多个重载的构造函数,它们并非调用不同的超类构造函数,并且不能简化为具有默认参数值的单个构造函数,那么优先用工厂函数取代这些重载的构造函数。
平台类型
返回平台类型表达式的公有函数/方法必须显式声明其 Kotlin 类型:
fun apiCall(): String = MyJavaApi.getProperty("name")
任何使用平台类型表达式初始化的属性(包级别或类级别)必须显式声明其 Kotlin 类型:
class Person {
val name: String = MyJavaApi.getProperty("name")
}
使用平台类型表达式初始化的局部值可以有也可以没有类型声明:
fun main() {
val name = MyJavaApi.getProperty("name")
println(name)
}
使用作用域函数 apply/with/run/also/let
Kotlin 提供了一系列用来在给定对象上下文中执行代码块的函数:let
、 run
、 with
、 apply
以及 also
。
关于不同情况下选择正确作用域函数的准则,请参考作用域函数。
库的编码规范
在编写库时,建议遵循一组额外的规则以确保 API 的稳定性:
- 总是显式指定成员的可见性(以避免将声明意外暴露为公有 API )
- 总是显式指定函数返回类型以及属性类型(以避免当实现改变时
意外更改返回类型) - 为所有公有成员提供 KDoc 注释,不需要任何新文档的覆盖成员除外
(以支持为该库生成文档)