给安卓开发者的 Kotlin tips

701 阅读5分钟

with & apply

打印如下字符

START
ABCDEFGHIJKLMNOPQRSTUVWXYZ
END

如果不考虑kotlin的特性直接写成java风格的代码如下

fun alphabet(): String {
    val result = StringBuilder()
    result.append("START\n")
    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\nEND")
    return result.toString()
}

可以使用with简化代码

fun alphabet3(): String {
    return with(StringBuilder()) {
        append("START\n")
        for (letter in 'A'..'Z') {
            this.append(letter)
        }
        append("\nEND")
        toString()
    }
}

with函数内部通过this来访问传入的参数,这里具体指的是StringBuilder()的结果,with的返回值就是花括号的最后一行,也就是StringBuilder.toString()

另外with里面的this可以省去

    fun alphabet4() =
        with(StringBuilder()) {
            append("START\n")
            for (letter in 'A'..'Z') {
                this.append(letter)
            }
            append("\nEND")
            toString()
        }

apply

有一个对象

class User {
    var firstName = ""
    var lastName = ""
    var age = 10
    var address = "SH"
}

现在要对其初始化,可以像java那样

var user2 = User()
user2.firstName = "chen"
user2.lastName = "si"
user2.age = 30
user2.address = "BJ"

kotlin中为我们提供了apply方法,方便一系列的初始化

var user = User().apply {
    firstName = "chen"
    lastName = "si"
    age = 30
    address = "BJ"
}

对一个TextView初始化就变成了这样

    textView.apply { 
            text = "hello"
            textSize = 32f
            textAlignment = TEXT_ALIGNMENT_CENTER
        }

with vs apply

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

通过with的函数声明可以看出来,with接受两个参数,并且在第二个参数里面可以直接访问第一个参数的方法,with的返回值是第二个参数,也就是第二个代码块最后一行

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

apply函数是个扩展函数,接受一个参数,返回值就是参数自身。

解构声明

val (red, green, blue) = Color.RED
val (left, top, right, bottom) = Rect(10, 20, 30, 40)
val (x, y) = Point(10, 20)

显式参数名及默认参数值

fun <T> joinToString(collection: Collection<T>,
                     separator: String,
                     prefix: String,
                     postfix: String): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

    val list = listOf("one", "two", "three", "four", "five")
    // 不标明参数名
    println(joinToString(list, " - ", "[", "]"))

这段代码把list按顺序输出,最前面加上"[" ,中间加上" - ",最后加上"]",输出结果为

[one - two - three - four - five]

这样写的问题在于要传递过多参数,当然可以像java那样提供多个重载版本来实现默认参数的目的,kotlin中直接提供了默认参数的语法


fun <T> joinToString2(collection: Collection<T>,
                      separator: String = ", ",
                      prefix: String = "",
                      postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

println(joinToString2(list, " - "))
println(joinToString2(list, " , ", "["))

输出为

[one - two - three - four - five]
one - two - three - four - five

joinToString2(list, " - ")指定了separator的值为 " - ",其他没有指定的均使用默认值。

joinToString2(list, " , ", "[")指定了separator和prefix,而postfix使用默认值。

另外可以使用扩展函数进一步简化代码


fun <T> Collection<T>.joinToString3(separator: String = ", ",
                                    prefix: String = "",
                                    postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
    
println(list.joinToString3("/"))

有个默认参数之后可以大大简化安卓中自定义view中构造函数

class MyLinearLayout2 constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr)

运算符重载

class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }

    override fun toString(): String {
        return "Point(x=$x, y=$y)"
    }
}

重写plus方法,前面加上operator关键字, 然后在函数里面定义具体的实现,之后就可以使用 加好直接操作对象了。


    val point1 = Point(10, 10)
    val point2 = Point(4, 4)
    val point3 = point1 + point2

再看个示例:

        val spanString = "Copyright".toSpannable()
        spanString.setSpan(StyleSpan(BOLD), 0, spanString.length, SPAN_INCLUSIVE_EXCLUSIVE)
        spanString.setSpan(UnderlineSpan(), 0, spanString.length, SPAN_INCLUSIVE_EXCLUSIVE)
        spanString.setSpan(StyleSpan(Typeface.ITALIC), 0, spanString.length, SPAN_INCLUSIVE_EXCLUSIVE)
        textView.setText(spanString)

这段代码 给TextView的上显示的文字一次加上 加粗,下划线以及斜体的效果 借助运算符重载和ktx这段代码也可以简化


        val spannable = "Eureka!!!!".toSpannable()
        spannable += StyleSpan(BOLD) // Make the text bold with +=
        spannable += UnderlineSpan() // Make the text underline with +=
        spannable += StyleSpan(ITALIC)
        textView.setText(spanString)

lambda

点击事件一般这么写


button.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View) {
        handleClick(v)
    }
})

有了lambda之后可以这么写

    button.setOnClickListener({ v ->
            {
                handleClick(v)
            }
        })

参数v可以去掉,直接使用it代替


    button.setOnClickListener({
            handleClick(it)
        })

如果最后一个参数是高阶函数,可以直接移出小括号里面


    button.setOnClickListener() {
            handleClick(it)
        }
        

如果只有一个参数,并且这个参数是高阶函数,可以把小括号移除

    button.setOnClickListener {
            handleClick(it)
        }

高阶函数

看一下前面的一个扩展函数


fun <T> Collection<T>.joinToString3(separator: String = ", ",
                                    prefix: String = "",
                                    postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

这个函数的问题在于扩展性较差,目前只能把element的toString的结果加到最终的输出,如果想留出自己实现该怎么办呢? 这时候就需要高阶函数出场了

/扩展函数
fun <T> Collection<T>.joinToString(separator: String = " ,", prefix: String = " ", postfix: String = " ",
                                   transform: (T) -> String = { it.toString() }): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(transform(element))
    }
    result.append(postfix)
    return result.toString()
}

    var listOf = listOf("Chen", "Rui", "Feng")
    
    println(listOf.joinToString())
    println(listOf.joinToString { it.toLowerCase() })
    println(listOf.joinToString(transform = { it.toUpperCase() }))
 Chen ,Rui ,Feng 
 chen ,rui ,feng 
 CHEN ,RUI ,FENG 

通过高阶函数可以把自己需要的输出方式传递进去

再看个例子

    data class SiteVisit(val path: String,
                     val duration: Double,
                     val os: OS)
                     
    enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
    
    val log = listOf(
        SiteVisit("/", 34.0, OS.WINDOWS),
        SiteVisit("/", 22.0, OS.MAC),
        SiteVisit("/login", 12.0, OS.WINDOWS),
        SiteVisit("/signup", 8.0, OS.IOS),
        SiteVisit("/", 16.3, OS.ANDROID)
    )

定义一个类SiteVisit 有三个参数,分别表示用户访问的路径,访问持续时间以及用户的操作系统

现在要统计出操作系统是WINDOWS的用户的平均访问时间

    log.filter { it.os == OS.WINDOWS }
            .map { it.duration }
            .average()

要统计出操作系统是MAC的用户的平均访问时间

    log.filter { it.os == OS.MAC }
            .map { it.duration }
            .average()

如果统计IOS用户的平均访问时间呢?再写一遍? 可以抽取出一个函数出来

fun List<SiteVisit>.averageDurationFor(os: OS) =
        filter { os == it.os }
                .map { it.duration }
                .average()

调用的时候直接传入操作系统类型就行

    log.averageDurationFor(OS.WINDOWS)
    log.averageDurationFor(OS.MAC)

现在问题来了,

  • 统计出WINDOWSMAC用户的平均访问时间
  • 统计出IOS用户访问/login页面的平均访问时间

刚刚的函数就没办法使用了,这时候就需要高阶函数了

fun List<SiteVisit>.averageDurationFor2(predicate: (SiteVisit) -> Boolean) =
        filter { predicate(it) }
                .map { it.duration }
                .average()

调用的时候直接把条件传进去


    //android and ios average time
    println(log.averageDurationFor2 { it.os == OS.ANDROID || it.os == OS.IOS })


    //ios & login page average time
    println(log.averageDurationFor2 { it.os == OS.WINDOWS && it.path == "/login" })

有了这个高阶函数 最开始统计出操作系统是MAC的用户的平均访问时间的代码可以写成这样子

    log.averageDurationFor2 { it.os == OS.MAC })

参考文档:

Kotlin in action