Kotlin The Right Way

Blog

现在越来越多的人开始跟风使用Kotlin作为他们Android开发的第二语言,但在使用的时候却完全是Java的风格,给我的感觉就像连官方文档都没有看就拿起来开发了,真是可惜了这门语言,所以写这篇文章的目的就是列举使用Kotlin的正确姿势,帮助Android程序员写代码时Thinking in Kotlin。

这篇文章会列举实战中的例子,并不会像官方文档一样一步步详细介绍Kotlin,想学习Kotlin请移步这里

我找了一个Github上用Kotlin写的项目,wechat_no_revoke(下面称作wnr),这个项目作为负面教材简直完美,下面的文章主要就指出其中的问题(只是语言使用上的问题,不涉及它实现的功能--防微信撤销)

use let instead of if(sth != null)

wnr中充斥着if (sth != null) { doSomething() }(看WechatRevokeHook.kt),Kotlin提供的标准库,可以完美解决这种冗余代码。

The Right Way:所有用if判断对象是否为null的都用let代替:

sth?.let { doSomething() }

let体内的代码只有在sth不为null时才会运行。实际上这个let不是什么Kotlin编译器里的特殊语法,它只是一个普通的函数,你自己都可以实现一个let,下面是标准库里的实现:

public inline fun <T, R> T.let(block: (T) -> R): R = block(this)

let是一个任意类型T上的扩展函数,参数是一个Lambda,然后再在Lambda上调用自己。这样说可能理解起来不那么容易,其实let是一个scoping function,它保护了let体内的变量不泄露到外界,null检查只是附带的功能,举个例子:

DbConnection.getConnection().let { connection ->
}
// connection到这里就访问不到了

use apply to simplify your code

WecahtDatebase.kt中,有这样一段代码:

val v = ContentValues()
v.put("msgid", msgId)
......
v.put("content", msg)
if (talkerId != -1) {
    v.put("talkerid", talkerId)
}
insert("message", "", v)

同样Kotlin标准库中提供了apply函数,专门用来应付这种命令式的代码。

The Right Way:

ContentValues().apply {
    put("msgid", msgId)
    ......
    put("content", msg)
    if (talkerId != -1) {
        put("talkerid", talkerId)
    }
    insert("message", "", this)
}

同样apply也不是什么神奇的东西,它只是个普通的函数:

fun <T> T.apply(f: T.() -> Unit): T { f(); return this }

apply定义了一个所有类型上的扩展方法,调用apply的时候,会调用传进去的闭包,并返回在闭包上运行过的receiver对象。其实不是那么复杂,看下面的例子你就懂了:

//把string转为File对象,对此对象调用mkdirs()方法,最后返回此对象
File(dir).apply { mkdirs() }

//下面是等同的Java代码
File makeDir(String path) {
  File result = new File(path);
  result.mkdirs();
  return result;
}

能用一行解决的就不要用多行。

既然标准库说了这么多,就顺便说完吧:

//如果那要对同一个对象多次调用不同的方法,就用with
fun <T, R> with(receiver: T, f: T.() -> R): R = receiver.f()

val w = Window()
with(w) {
  setWidth(100)
  setHeight(200)
  setBackground(RED)
}

//用run表示链式调用(run是with和let的合体)
fun <T, R> T.run(f: T.() -> R): R = f()

"123".run { print(this) }   
        .run { print("hehe") }   //输出"123hehe"
        
//用use得到与java try-with-resources一样的效果(资源会自动close),注意这里的use也只是个普通的函数而已,不像java一样要编译器用特殊的语法才能做到:
fun readProperties() = Properties().apply {
    FileInputStream("config.properties").use { 
    fis ->
        load(fis)
    }
}

//下面是java 1.7及以上才有的try-with-resources
Properties prop = new Properties();
try (FileInputStream fis = new FileInputStream("config.properties")) {
    prop.load(fis);
}
// fis automatically closed

use ? to indicate Nullable carefully

wnr中有这样一段代码(看MessageUtil.kt):

fun extractContent(replace: String?, str: String?): String? {
        var _replace = replace!!
        var _str = str!!
        ......
        ... do something with _replace and _str
        ......
        return _replace
}

我不知道这哥们写的时候怎么想的,!!是程序员知道对象不可能为null时才用来强转为非null变量的(如果是null程序就崩了),而既然知道不可能为null,那为什么还要用?来表示参数可能为null呢,而且返回值居然带问号,excuse me?互相矛盾...无语。这样的问题充斥这整个项目,完全是乱的。

The Right way:

fun extractContent(replace: String, str: String): String {
        ......
        ... do something with replace and str
        ......
        return replace
}
val str1 = "str1" //str1类型为`String`,不可能为null
var str2: String? = null //str2类型为`String?`,现在初始化是null,以后也可能是null
str2 = "some value"  //str2还是`String?`,只不过现在值不是null了
val str3 = str2!! //str3类型为`String`,这里这能在你确定str2不是null的情况下才能用,编译器并不能保证str2不是null

use first class function instead of object

既然上面说到MessageUtil了,那顺便说说这个问题。在Kotlin中,函数也是第一公民,下面是wnr中的代码:

//MessageUtil.kt:
object MessageUtil {
    fun extractContent(......): String? {
    ......
    }
}

//Some other file:
content = MessageUtil.extractContent(replaceMsg, content)!!

就不说这个!!了,上面说过,全是乱的。我就说说最好笑的,这个object MessageUtil完全是多余的,在FP里,函数是第一公民,意味着你不必把方法写在类里,函数也是值,可以做参数,可以当返回值,可以独立于类存在(其实编译成class后函数也在类里,不过这对用户来说是透明的)。

The Right Way:

//MessageUtil.kt:
fun extractContent(......): String {
......
}

//Some other file:
content = extractContent(replaceMsg, content)

use primary constructor instead of java style constructor

下面的是wnr中的WechatRevokeHook.kt

class WechatRevokeHook {

    var _v: WechatVersion? = null
    
    constructor(ver: WechatVersion) {
        _v = ver
    }
    ......
}

相信大多数人只要看过文档都不会写出这样的代码。

The Right Way: 用primary constructor替代:

class WechatRevokeHook(val ver: WechatVersion) {
    ......
}

use Delegates to initialize field

下面是wnr中的代码

class MainActivity : Activity(),... {
    ......
    private var tvVersion: TextView? = null
    private var tvProj: TextView? = null
    private var tvRepo1: TextView? = null
    private var tvRepo2: TextView? = null
    ......
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main)
        tvVersion = findViewById(R.id.tvVersion) as TextView?
        tvProj = findViewById(R.id.tvProj) as TextView?
        tvRepo1 = findViewById(R.id.tvRepo1) as TextView?
        tvRepo2 = findViewById(R.id.tvRepo2) as TextView?
        ......
    }
}

在下有一万种方法优化这坨代码(笑),首先,最简单的,用lazy delegate:

val tvVersion by lazy { findViewById(R.id.tvVersion) as TextView }

这样tvVersion只有在第一个用的时候才会初始化,以前分离的声明与初始化合在的一起,只有一行,更加优美,便于理解,而且没有null的烦恼,tvVersion既是val(不会变),又是TextView(没有?,不可能是null),更加安全。

但是这里代码还是有点长,又要写lazy,又要强转View为TextView,这些代码我都不想写,有没有更简单的写法呢?答案是肯定的,只需要自己实现一个类似lazy的Delegate就可以了,注意,这里的lazy不是编译器里什么神奇的东西,它也是一个方法。

//ButterKnife.kt
public fun <V : View> Activity.bindView(id: Int)
        : ReadOnlyProperty<Activity, V> = required(id, viewFinder)
        
private val Activity.viewFinder: Activity.(Int) -> View?
    get() = { findViewById(it) }
        
private fun <T, V : View> required(id: Int, finder: T.(Int) -> View?)
        = Lazy { t: T, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) }
        
// Like Kotlin's lazy delegate but the initializer gets the target and metadata passed to it
private class Lazy<T, V>(private val initializer: (T, KProperty<*>) -> V) : ReadOnlyProperty<T, V> {
    private object EMPTY

    private var value: Any? = EMPTY

    override fun getValue(thisRef: T, property: KProperty<*>): V {
        if (value == EMPTY) {
            value = initializer(thisRef, property)
        }
        @Suppress("UNCHECKED_CAST")
        return value as V
    }
}

这样再在Activity里,就可以这样用:

val tvVersion by bindView<TextView>(R.id.tvVersion)

再也没有findViewById的烦恼啦。

上面那段代码来自Jake Wharton,是的,Jake Wharton用一个文件就解决了Butterknife Java版解决的问题,我曾经深入的研究过Java版Butterknife,还写了一个类似Butterknife的工具,Butterknife要在编译期用AnnotationProcessor处理java文件中的annotation,然后利用javapoet生成代码,在生成的代码中findViewById并进行绑定,其中涉及到的apt以及代码生成会影响到性能,而Kotlin并没有这些问题。

DSL

就Kotlin展开的话,设计到了Functional Programming和DSL,前者这里就不在展开讨论了,大牛太多,后者我可以简单介绍下,毕竟不同语言构造DSL的方式都不大相同。

不少人都用Retrofit,就拿Retrofit举个例子吧,下面是一个简单的Retrofit DSL,最终的效果是这样的:

fun httpService(base: String) =
        retrofit {
            client {
                readTimeout = sc(100)
                connectTimeout = sc(100)
                headers {
                    "Api-Version" with "dim"
                    "Origin" with "hehe"
                    "Token" with PreferencesUtils.getToken()
                }
                sslCert(Application.getInstance().applicationContext) {
                    strong = true
                    certs = listOf(R.raw.https)
                }
            }
            hostUrl = ServerUtil.getCurrentServerBase();
            baseUrl = base
            converterFactories = listOf(GsonConverterFactory.create())
        }

val services: EnumPoll<TEST, Retrofit, String>
    get() = poll {
        mapping {
            TEST.A with "baseUrlA/"
            TEST.B with "baseUrlB/"
        }
        instance = ::httpService
    }
    
fun testRetrofitDSL() {
    testservice<ApiTest>().testBaidu().enqueue {
        onResponse { res ->
            //doSomethingWith Res
        }

        onFailure { throwable ->
            //doSomethingWhen failure
        }
    }
}

上面是合法的Kotlin代码,函数httpService返回一个Retrofit实例,client返回一个OkHttpClient,其中包含了header、https(SSL)等设置,services是包含Retrofit实例和baseUrl的HashMap,会重复利用有相同baseUrl的Retrofit对象,Api的baseUrl是通过annotation反射获得的,annotation的参数是个Enum,testRetrofitDSL是最终使用时的例子。完整实现代码在这里,有兴趣的可以看一看,还是蛮有意思的。

总结

Kotlin虽然入门简单,但是实际用起来还是需要使用者花点心思的,不然写出来的代码就是换个样子的Java。Functional Programming、DSL、Extention Function、Null Safety这些概念虽然不是什么新的点子,但Kotlin却用自己独特的实现方式,让这些特性都有很好的用武之地,不能好好利用就很可惜了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容