第一行代码 -- 笔记1

一、快速入门 Kotlin 编程

1.变量和函数

变量

在 Kotlin 中定义一个变量,只允许在变量前声明两种关键字:val 和 var。

  • val( value 的简写)用来声明一个不可变的变量,这种变量在初始化赋值之后就再也不能重新赋值,对应 Java 中的 final 变量。

  • var( variable 的简写)用来声明一个可变的变量,这种变量在初始化赋值之后仍然可以再被重新赋值,对应 Java 中的非 final 变量。

Kotlin 拥有出色的类型推导机制,但是 Kotlin 的类型推导机制并不总是可以正常工作的,比如说如果我们对一个变量延迟赋值的话,Kotlin 就无法自动推导它的类型了,这时候就需要显式地声明变量类型才行:

fun main() {
    val a = 10 //可以进行类型推导
    var b: String //无法进行类型推导,显式声明变量类型
    println("a: " + a)
    b = "hello"
    println("b: " + b)
}

Kotlin 完全抛弃了 Java 中的基本数据类型,全部使用了对象数据类型。Java 和 Kotlin 数据类型对照表如下:

Java 基本数据类型 Kotlin 对象数据类型 数据类型说明
int Int 整型
long Long 长整型
short Short 短整型
float Float 单精度浮点型
double Double 双精度浮点型
boolean Boolean 布尔型
char Char 字符型
byte Byte 字节型
函数

语法规则:

fun methodName(param1: Int, param2: Int): Int {
    return 0
}

举个例子:

package com.tomorrow.kotlindemo

import kotlin.math.max

fun main() {
    val a = 37
    val b = 40
    val value = largerNumber(a, b)
    println("larger number: " + value)
}

fun largerNumber(num1: Int, num2: Int): Int {
    return max(num1, num2)
}

日志打印:
larger number: 40

当一个函数中只有一行代码时,Kotlin 允许我们不必编写函数体,可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可:

fun largerNumber(num1: Int, num2: Int): Int = max(num1, num2)

函数返回值可以进行类型推导:

fun largerNumber(num1: Int, num2: Int) = max(num1, num2)

2.程序的逻辑控制

if 条件语句

举个例子:

package com.tomorrow.kotlindemo

fun main() {
    val a = 37
    val b = 40
    val value = largerNumber(a, b)
    println("larger number: " + value)
}

fun largerNumber(num1: Int, num2: Int): Int {
    var value = 0
    if (num1 > num2) {
        value = num1
    } else {
        value = num2
    }
    return value
}

日志打印:
larger number: 40

Kotlin 中的 if 语句相比于 Java 有一个额外的功能,它是可以有返回值的,返回值就是 if 语句每一个条件中最后一行代码的返回值:

fun largerNumber(num1: Int, num2: Int): Int {
    return if (num1 > num2) {
        num1
    } else {
        num2
    }
}

进一步精简:

fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) {
    num1
} else{
    num2
}

fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) num1 else num2
when 条件语句

语法规则:

匹配值 -> { 执行逻辑 } //当执行逻辑只有一行代码时,{ }可以省略

举个例子:

package com.tomorrow.kotlindemo

fun main() {
    val score = getScore("Jim")
    println("score: " + score)
}

fun getScore(name: String) = when (name) {
    "Tom" -> 86
    "Jim" -> 77
    "Jack" -> 95
    "Lily" -> 100
    else -> 0
}

日志打印:
score: 77

when 语句不带参数的用法:

fun getScore(name: String) = when {
    name.startsWith("Tom") -> 86
    name == "Jim" -> 77
    name == "Jack" -> 95
    name == "Lily" -> 100
    else -> 0
}
循环语句

举个例子:

package com.tomorrow.kotlindemo

fun main() {
    for (i in 0..3) { //双端闭区间
        println(i)
    }
}

日志打印:
0
1
2
3
package com.tomorrow.kotlindemo

fun main() {
    for (i in 0 until 3) { //左闭右开区间
        println(i)
    }
}

日志打印:
0
1
2

使用 step 关键字:

package com.tomorrow.kotlindemo

fun main() {
    for (i in 0 until 3 step 2) {
        println(i)
    }
}

日志打印:
0
2

创建一个降序区间:

package com.tomorrow.kotlindemo

fun main() {
    for (i in 3 downTo 1) {
        println(i)
    }
}

日志打印:
3
2
1

for-in 循环除了可以对区间进行遍历之外,还可以用于遍历数组和集合。

3.面向对象编程

类与对象

举个例子:

//Person.kt
package com.tomorrow.kotlindemo

class Person {
    var name = ""
    var age = 0

    fun eat() {
        println(name + " is eating. He is " + age + " years old.")
    }
}

//LearnKotlin.kt
package com.tomorrow.kotlindemo

fun main() {
    var p = Person() //实例化一个类不需要 new 关键字
    p.name = "Jack"
    p.age = 19
    p.eat()
}

日志打印:
Jack is eating. He is 19 years old.
继承与构造函数

Kotlin 默认所有非抽象类都是不可以被继承的。之所以说非抽象类,是因为抽象类本身是无法创建实例的,一定要由子类去继承它才能创建实例,因此抽象类必须可以被继承才行,要不然就没有意义了。

举个例子:

open class Person { //添加 open 关键字
    var name = ""
    var age = 0

    fun eat() {
        println(name + " is eating. He is " + age + " years old.")
    }
}

class Student : Person() { //Person 类后面的一对括号表示 Student 类的主构造函数在初始化的时候会调用 Person 类的无参数构造函数,即使在无参数的情况下,这对括号也不能省略
    var sno = ""
    var grade = 0
}

var s = Student()

Kotlin 将构造函数分成了两种:主构造函数和次构造函数。主构造函数将会是你最常用的构造函数,每个类默认都会有一个不带参数的主构造函数,当然你也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即可:

class Student(val sno: String, val grade: Int) : Person() {
}

var s = Student("a123", 5)

Kotlin 给我们提供了一个 init 结构体,如果需要,所有主构造函数的逻辑都可以写在里面:

class Student(val sno: String, val grade: Int) : Person() {
    init {
        println("sno: " + sno)
        println("grade: " + grade)
    }
}

父类没有无参的构造函数情况:

open class Person(val name: String, val age: Int) {
    fun eat() {
        println(name + " is eating. He is " + age + " years old.")
    }
}

class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age)

var s = Student("a123", 5, "Jack", 19)

我们在 Student 类的主构造函数中增加 name 和 age 这两个字段时,不能再将它们声明成 val,因为在主构造函数中声明成 val 或者 var 的参数将自动成为该类的字段,这就会导致和父类中同名的 name 和 age 字段造成冲突。因此,这里的 name 和 age 参数前面我们不用加任何关键字,让它的作用域仅限定在主构造函数当中即可。

其实你几乎是用不到次构造函数的,Kotlin 提供了一个给函数设定参数默认值的功能,基本上可以替代次构造函数的作用。任何一个类只能有一个主构造函数,但是可以有多个次构造函数,次构造函数也可以用于实例化一个类,这一点和主构造函数没有什么不同,只不过它是有函数体的。Kotlin 规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用):

class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {
    constructor(name: String, age: Int) : this("", 0, name, age) {
    }

    constructor() : this("", 0) {
    }
}

var s = Student("a123", 5, "Jack", 19)
var s3 = Student("Jack", 19)
var s2 = Student()

特殊情况:当一个类没有显式地定义主构造函数且定义了次构造函数时,它就是没有主构造函数的。

class Student : Person { //没有显式定义主构造函数
    constructor(name: String, age: Int) : super(name, age) { //且定义了次构造函数
    }
}

Student 类是没有主构造函数的,继承 Person 类的时候也就不需要再加上括号了。另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数,使用 super 关键字,而不是 this 关键字。

接口

接口是用于实现多态编程的重要组成部分。举个例子:

//Person.kt
package com.tomorrow.kotlindemo

open class Person(val name: String, val age: Int)

//Study.kt
package com.tomorrow.kotlindemo

interface Study {
    fun readBooks()
    fun doHomework() {
        println("do homework default implementation")
    }
}

//Student.kt
package com.tomorrow.kotlindemo

class Student(name: String, age: Int) : Person(name, age), Study { //接口的后面不用加上括号,因为它没有构造函数可以去调用
    override fun readBooks() {
        println(name + " is reading.")
    }

    override fun doHomework() {
        println(name + " is doing homework.")
    }
}

//LearnKotlin.kt
package com.tomorrow.kotlindemo

fun main() {
    var s = Student("Jack", 19)
    doStudy(s)
}

fun doStudy(study: Study) {
    study.readBooks()
    study.doHomework()
}

日志打印:
Jack is reading.
Jack is doing homework.

对接口中的函数进行默认实现:

interface Study {
    fun readBooks()
    fun doHomework() {
        println("do homework default implementation")
    }
}

现在当一个类去实现 Study 接口时,只会强制要求实现 readBooks() 函数,而 doHomework() 函数则可以自由选择实现或者不实现,不实现时就会自动使用默认的实现逻辑。

Java 和 Kotlin 函数可见性修饰符对照表:

修饰符 Java Kotlin
public 所有类可见 所有类可见(默认)
private 当前类可见 当前类可见
protected 当前类、子类、同一包路径下的类可见 当前类、子类可见
default 同一包路径下的类可见(默认)
internal 同一模块中的类可见
数据类与单例类

数据类通常需要重写 equals()、hashCode()、toString() 这几个方法。其中,equals() 方法用于判断两个数据类是否相等。hashCode() 方法作为 equals() 的配套方法,也需要一起重写,否则会导致 HashMap、HashSet 等 hash 相关的系统类无法正常工作。toString() 方法用于提供更清晰的输入日志,否则一个数据类默认打印出来的就是一行内存地址。

当在一个类前面声明了 data 关键字时,就表明你希望这个类是一个数据类,Kotlin 会根据主构造函数中的参数帮你将 equals()、hashCode()、toString() 等固定且无实际逻辑意义的方法自动生成,从而大大减少了开发的工作量。

举个例子:

//CellPhone.kt
package com.tomorrow.kotlindemo

data class CellPhone(val brand: String, val price: Double)

//LearnKotlin.kt
package com.tomorrow.kotlindemo

fun main() {
    val cellphone1 = CellPhone("Samsung", 1999.0)
    val cellphone2 = CellPhone("Samsung", 1999.0)
    println(cellphone1)
    println("cellphone1 equals cellphone2: " + (cellphone1 == cellphone2))
}

日志打印:
CellPhone(brand=Samsung, price=1999.0)
cellphone1 equals cellphone2: true

在 Kotlin 中创建一个单例类的方式极其简单,只需要将 class 关键字改成 object 关键字即可。

举个例子:

//Singleton.kt
package com.tomorrow.kotlindemo

object Singleton {
    fun singletonTest() {
        println("singletonTest is called.")
    }
}

//LearnKotlin.kt
package com.tomorrow.kotlindemo

fun main() {
    Singleton.singletonTest()
}

日志打印:
singletonTest is called.

4.Lambda 编程

Java 直到 JDK 1.8 之后才加入了 Lambda 编程的语法支持,而 Kotlin 从第一个版本开始就支持了 Lambda 编程。

集合的创建与遍历

Kotlin 专门提供了一个内置的 listOf() 函数来简化初始化集合的写法:

var list = listOf("Apple", "Banana", "Orange")

for-in 循环不仅可以用来遍历区间,还可以用来遍历集合:

var list = listOf("Apple", "Banana", "Orange")

for(fruit in list) {
    println(fruit)
}

listOf() 函数创建的是一个不可变集合,不可变集合指的就是该集合只能用于读取,我们无法对集合进行添加、修改或删除操作。至于这么设计的理由,和 val 关键字、类默认不可继承的设计初衷是类似的,可见 Kotlin 在不可变性方面控制得极其严格。

可以使用 mutableListOf() 函数创建一个可变的集合:

var list = mutableListOf("Apple", "Banana", "Orange")
list.add("Watermelon")

for(fruit in list) {
    println(fruit)
}

Set 集合的用法几乎和 List 集合一模一样,只是将创建集合的方式换成了 setOf() 和 mutableSetOf() 函数而已。需要注意,Set 集合底层是使用 hash 映射机制来存放数据的,因此集合中的元素无法保证有序,这是和 List 集合最大的不同之处。

Kotlin 提供了一对 mapOf() 和 mutableMapOf() 函数来继续简化 Map 的用法。在 Kotlin 中并不建议使用 put() 和 get() 方法来对 Map 进行添加和读取数据操作,而是更加推荐使用一种类似于数组下标的语法结构:

var map = mutableMapOf("Apple" to 1, "Banana" to 2, "Orange" to 3)

for((fruit, number) in map) {
    println("fruit: " + fruit + ", number: " + number)
}

map["Apple"] = 10
val number = map["Apple"]
println("Apple number: " + number)
集合的函数式 API

Lambda 表达式的语法结构:

{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}

最后一行代码会自动作为 Lambda 表达式的返回值。

举个例子:

val list = listOf("Apple", "Banana", "Orange")
val maxLengthFruit = list.maxBy { it.length }
println("max length fruit: " + maxLengthFruit)

带 Lambda 参数的函数调用过程简化:

  • 初始状态
val list = listOf("Apple", "Banana", "Orange")
val lambda = { fruit: String -> fruit.length }
val maxLengthFruit = list.maxBy(lambda)
  • 省略 lambda 变量
val list = listOf("Apple", "Banana", "Orange")
val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length })
  • 当 Lambda 参数是函数的最后一个参数时,可以将 Lambda 表达式移到函数括号的外面
val list = listOf("Apple", "Banana", "Orange")
val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length }
  • 如果 Lambda 参数是函数的唯一一个参数的话,可以将函数的括号省略
val list = listOf("Apple", "Banana", "Orange")
val maxLengthFruit = list.maxBy { fruit: String -> fruit.length }
  • 由于 Kotlin 拥有出色的类型推导机制,Lambda 表达式中的参数列表其实在大多数情况下不必声明参数类型
val list = listOf("Apple", "Banana", "Orange")
val maxLengthFruit = list.maxBy { fruit -> fruit.length }
  • 当 Lambda 表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用 it 关键字来代替
val list = listOf("Apple", "Banana", "Orange")
val maxLengthFruit = list.maxBy { it.length }

集合中的 map 函数是最常用的一种函数式 API,它用于将集合中的每个元素都映射成一个另外的值,映射的规则在 Lambda 表达式中指定,最终生成一个新的集合:

fun main() {
    val list = listOf("Apple", "Banana", "Orange")
    val newList = list.map { it.toUpperCase() }
    for (fruit in newList) {
        println(fruit)
    }
}

日志打印:
APPLE
BANANA
ORANGE

filter 函数是用来过滤集合中的数据的,它可以单独使用,也可以配合 map 函数一起使用:

fun main() {
    val list = listOf("Apple", "Banana", "Orange")
    val newList = list.filter { it.length <= 5 }
                      .map { it.toUpperCase() }
    for (fruit in newList) {
        println(fruit)
    }
}

日志打印:
APPLE

any 函数用于判断集合中是否至少存在一个元素满足指定条件,all 函数用于判断集合中是否所有元素都满足指定条件:

fun main() {
    val list = listOf("Apple", "Banana", "Orange")
    val anyResult = list.any { it.length <= 5 }
    val allResult = list.all { it.length <= 5 }
    println("anyResult: " + anyResult + ", allResult: " + allResult)
}

日志打印:
anyResult: true, allResult: false
Java 函数式 API 的使用

如果我们在 Kotlin 代码中调用了一个 Java 方法,并且该方法接收一个 Java 单抽象方法接口参数,就可以使用函数式 API。Java 单抽象方法接口指的是接口中只有一个待实现方法,如果接口中有多个待实现方法,则无法使用函数式 API。

举个例子:

fun main() {
    Thread(object : Runnable {
        override fun run() {
            println("Thread is running")
        }
    }).start()
}

日志打印:
Thread is running

Kotlin 中匿名类的写法和 Java 有一点区别,由于 Kotlin 完全舍弃了 new 关键字,因此创建匿名类实例的时候就不能再使用 new 了,而是改用了 object 关键字。这种写法虽然算不上复杂,但是相比于 Java 的匿名类写法,并没有什么简化之处。

使用 Java 函数式 API 进行精简:

fun main() {
    Thread(Runnable {
        println("Thread is running")
    }).start()
}

因为 Runnable 类中只有一个待实现方法,即使这里没有显式地重写 run() 方法,Kotlin 也能自动明白 Runnable 后面的 Lambda 表达式就是要在 run() 方法中实现的内容。

另外,如果一个 Java 方法的参数列表中不存在一个以上 Java 单抽象方法接口参数,我们还可以将接口名进行省略:

fun main() {
    Thread({
        println("Thread is running")
    }).start()
}

进一步精简:

fun main() {
    Thread {
        println("Thread is running")
    }.start()
}

我们要经常打交道的 Android SDK 还是使用 Java 语言编写的,当我们在 Kotlin 中调用这些 SDK 接口时,就很可能会用到这种 Java 函数式 API 的写法:

button.setOnClickListener {
}

Java 函数式 API 的使用仅限定于从 Kotlin 中调用 Java 方法,并且单抽象方法接口也必须是用 Java 语言定义的。这是因为 Kotlin 中有专门的高阶函数来实现更加强大的自定义函数式 API 功能,从而不需要像 Java 这样借助单抽象方法接口来实现。

5.空指针检查

可空类型系统

Kotlin 将空指针异常的检查提前到了编译时期,如果我们的程序存在空指针异常的风险,那么在编译的时候会直接报错,修正之后才能成功运行,这样就可以保证程序在运行时期不会出现空指针异常了。Kotlin提供了另外一套可为空的类型系统(在类名的后面加上一个问号),只不过在使用可为空的类型系统时,我们需要在编译时期就将所有潜在的空指针异常都处理掉,否则代码将无法编译通过。

判空辅助工具

?. 操作符

fun doStudy(study: Study?) {
    study?.readBooks()
    study?.doHomework()
}

?: 操作符

fun getTextLength(text: String?) = text?.length ?: 0

!! 非空断言工具

var content: String? = "hello"

fun main() {
    if(content != null) {
        printUpperCase()
    }
}

fun printUpperCase() {
    val upperCase = content!!.toUpperCase() //在对象的后面加上 !!
    println(upperCase)
}

let 函数提供了函数式 API 的编程接口,并将原始调用对象作为参数传递到 Lambda 表达式中:

fun doStudy(study: Study?) {
    study?.let {
        it.readBooks()
        it.doHomework()
    }
}

let 函数是可以处理全局变量的判空问题的,而 if 判断语句则无法做到这一点:

//使用 let 函数可以正常工作
var study: Study? = null

fun doStudy() {
    study?.let {
        it.readBooks() //正常
        it.doHomework() //正常
    }
}

//使用 if 判断语句则会提示错误
var study: Study? = null

fun doStudy() {
    if(study != null) {
        study.readBooks() //报错,因为全局变量的值随时都有可能被其他线程所修改
        study.doHomework() //报错,因为全局变量的值随时都有可能被其他线程所修改
    }
}

6.小技巧

字符串内嵌表达式

举个例子:

fun main() {
    val brand = "Samsung"
    val price = 1999.0
    println("brand: $brand, price: $price")
}

日志打印:
brand: Samsung, price: 1999.0
函数的参数默认值

我们可以在定义函数的时候给任意参数设定一个默认值,这样当调用此函数时就不会强制要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。

举个例子:

fun main() {
    printParams(1, "aaa")
    printParams(2)
    printParams(num = 3, str = "ccc")
    printParams(str = "ddd", num = 4)
}

fun printParams(num: Int, str: String = "xxx") {
    println("num: $num, str: $str")
}

日志打印:
num: 1, str: aaa
num: 2, str: xxx
num: 3, str: ccc
num: 4, str: ddd

二、Activity

1.Activity 的基本用法

在 Activity 中使用 Toast
//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button1.setOnClickListener {
            Toast.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT).show()
        }
    }
}

使用 Kotlin 编写的 Android 项目在 app/build.gradle 文件的头部默认引入了一个 kotlin-android-extensions 插件,这个插件会根据布局文件中定义的控件 id 自动生成一个具有相同名称的变量,我们可以在 Activity 里直接使用这个变量,而不用再调用 findViewById() 方法了。

在 Activity 中使用 Menu
//res/menu/main.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/add_item"
        android:title="Add"/>
    <item
        android:id="@+id/remove_item"
        android:title="Remove"/>
</menu>

//MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.main, menu) //menuInflater 使用了语法糖,实际上是调用了父类的 getMenuInflater() 方法
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        when (item?.itemId) { //itemId 使用了语法糖,实际上是调用了 item 的 getItemId() 方法
            R.id.add_item -> Toast.makeText(this, "You clicked Add", Toast.LENGTH_SHORT).show()
            R.id.remove_item -> Toast.makeText(this, "You clicked Remove", Toast.LENGTH_SHORT).show()
        }
        return true
    }
}
使用 Intent 在 Activity 之间穿梭

使用显式 Intent:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button1.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            startActivity(intent)
        }
    }
}

使用隐式 Intent:

//AndroidManifest.xml
<activity android:name=".SecondActivity"
    android:label="This is SecondActivity">
    <intent-filter>
        <action android:name="com.tomorrow.action.START" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

//MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button1.setOnClickListener {
            val intent = Intent("com.tomorrow.action.START")
            startActivity(intent)
        }
    }
}

只有 <action> 和 <category> 中的内容同时匹配 Intent 中指定的 action 和 category 时,这个 Activity 才能响应该 Intent。android.intent.category.DEFAULT 是一种默认的 category,在调用 startActivity() 方法的时候会自动将这个 category 添加到 Intent 中。

更多隐式 Intent 的用法

调用系统的浏览器来打开网页:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button1.setOnClickListener {
            val intent = Intent(Intent.ACTION_VIEW)
            intent.data = Uri.parse("https://www.baidu.com")
            startActivity(intent)
        }
    }
}

还可以在 <intent-filter> 标签中再配置一个 <data> 标签,用于更精确地指定当前 Activity 能够响应的数据。<data> 标签中主要可以配置以下内容:

  • android:scheme:用于指定数据的协议部分,如 https。
  • android:host:用于指定数据的主机名部分,如 www.baidu.com
  • android:port:用于指定数据的端口部分,一般紧随在主机名之后。
  • android:path:用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。
  • android:mimeType:用于指定可以处理的数据类型,允许使用通配符的方式进行指定。

只有当 <data> 标签中指定的内容和 Intent 中携带的 Data 完全一致时,当前 Activity 才能够响应该 Intent:

//AndroidManifest.xml
<activity android:name=".SecondActivity"
    android:label="This is SecondActivity">
    <intent-filter tools:ignore="AppLinkUrlError">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="https" />
    </intent-filter>
</activity>

//MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button1.setOnClickListener {
            val intent = Intent(Intent.ACTION_VIEW)
            intent.data = Uri.parse("https://www.baidu.com")
            startActivity(intent)
        }
    }
}

调用系统拨号界面:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button1.setOnClickListener {
            val intent = Intent(Intent.ACTION_DIAL)
            intent.data = Uri.parse("tel:10086")
            startActivity(intent)
        }
    }
}

向下一个 Activity 传递数据:

//MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button1.setOnClickListener {
            val data = "Hello SecondActivity"
            val intent = Intent(this, SecondActivity::class.java)
            intent.putExtra("extra_data", data)
            startActivity(intent)
        }
    }
}

//SecondActivity.kt
class SecondActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        val extraData = intent.getStringExtra("extra_data")
        Log.d("SecondActivity", "extra_data: $extraData")
    }
}

返回数据给上一个 Activity:

//MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button1.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            startActivityForResult(intent, 1)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            1 -> if (resultCode == Activity.RESULT_OK) {
                val returnData = data?.getStringExtra("data_return")
                Log.d("MainActivity", "zwm, returned data: $returnData")
            }
        }
    }
}

//SecondActivity.kt
class SecondActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        button2.setOnClickListener {
            val intent = Intent()
            intent.putExtra("data_return", "Hello MainActivity")
            setResult(Activity.RESULT_OK, intent)
            finish()
        }
    }
}

2.Activity 的生命周期

返回栈

Android 是使用任务(task)来管理 Activity 的,一个任务就是一组存放在栈里的 Activity 的集合,这个栈也被称作返回栈(back stack)。栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的 Activity,它就会在返回栈中入栈,并处于栈顶的位置。而每当我们按下 Back 键或调用 finish() 方法去销毁一个 Activity 时,处于栈顶的 Activity就会出栈,前一个入栈的 Activity 就会重新处于栈顶的位置。系统总是会显示处于栈顶的 Activity 给用户。

生命周期
  • onCreate():在 Activity 第一次被创建的时候调用。你应该在这个方法中完成 Activity 的初始化操作,比如加载布局、绑定事件等。
  • onStart():在 Activity 由不可见变为可见的时候调用。
  • onResume():在 Activity 准备好和用户进行交互的时候调用。此时的 Activity 一定位于返回栈的栈顶,并且处于运行状态。
  • onPause():在系统准备去启动或者恢复另一个 Activity 的时候调用。我们通常会在这个方法中将一些消耗 CPU 的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶 Activity 的使用。
  • onStop():在 Activity 完全不可见的时候调用。它和 onPause() 方法的主要区别在于,如果启动的新 Activity 是一个对话框式的 Activity,那么 onPause() 方法会得到执行,而 onStop() 方法并不会执行。
  • onDestroy():在 Activity 被销毁之前调用,之后 Activity 的状态变为销毁状态。
  • onRestart():在 Activity 由停止状态变为运行状态之前调用,也就是 Activity 被重新启动了。
Activity 被回收数据保存与恢复
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if(savedInstanceState != null) {
            Log.d("MainActivity", "zwm, extra_data: ${savedInstanceState.getString("extra_data")}")
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        Log.d("MainActivity", "zwm, onSaveInstanceState")
        outState?.putString("extra_data", "onSaveInstanceState called")
    }
}

3.Activity 的启动模式

standard

在 standard 模式下,每当启动一个新的 Activity,它就会在返回栈中入栈,并处于栈顶的位置。对于使用 standard 模式的 Activity,系统不会在乎这个 Activity 是否已经在返回栈中存在,每次启动都会创建一个该 Activity 的新实例。

singleTop

当 Activity 的启动模式指定为 singleTop,在启动 Activity 时如果发现返回栈的栈顶已经是该 Activity,则认为可以直接使用它,不会再创建新的 Activity 实例。

singleTask

当 Activity 的启动模式指定为 singleTask,每次启动该 Activity 时,系统首先会在返回栈中检查是否存在该 Activity 的实例,如果发现已经存在则直接使用该实例,并把在这个 Activity 之上的所有其他 Activity 统统出栈,如果没有发现就会创建一个新的 Activity 实例。

singleInstance

指定为 singleInstance 模式的 Activity 会启用一个新的返回栈来管理这个 Activity(其实如果 singleTask 模式指定了不同的 taskAffinity,也会启动一个新的返回栈)。

//AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.tomorrow.kotlindemo">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".ThirdActivity"
            android:label="This is ThirdActivity"/>
        <activity
            android:name=".SecondActivity"
            android:label="This is SecondActivity"
            android:launchMode="singleInstance"/>
        <activity
            android:name=".MainActivity"
            android:label="This is MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

//MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d("MainActivity", "zwm, task id: $taskId")
        button1.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            startActivity(intent)
        }
    }

    override fun onResume() {
        super.onResume()
        Log.d("MainActivity", "zwm, onResume")
    }
}

//SecondActivity.kt
class SecondActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        Log.d("SecondActivity", "zwm, task id: $taskId")
        button2.setOnClickListener {
            val intent = Intent(this, ThirdActivity::class.java)
            startActivity(intent)
        }
    }

    override fun onResume() {
        super.onResume()
        Log.d("SecondActivity", "zwm, onResume")
    }
}

//ThirdActivity.kt
class ThirdActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_third)
        Log.d("ThirdActivity", "zwm, task id: $taskId")
    }

    override fun onResume() {
        super.onResume()
        Log.d("ThirdActivity", "zwm, onResume")
    }
}

日志打印:
2020-10-13 09:37:20.599 24160-24160/com.tomorrow.kotlindemo D/MainActivity: zwm, task id: 2245
2020-10-13 09:37:20.620 24160-24160/com.tomorrow.kotlindemo D/MainActivity: zwm, onResume
2020-10-13 09:37:25.915 24160-24160/com.tomorrow.kotlindemo D/SecondActivity: zwm, task id: 2246
2020-10-13 09:37:25.920 24160-24160/com.tomorrow.kotlindemo D/SecondActivity: zwm, onResume
2020-10-13 09:37:28.311 24160-24160/com.tomorrow.kotlindemo D/ThirdActivity: zwm, task id: 2245
2020-10-13 09:37:28.314 24160-24160/com.tomorrow.kotlindemo D/ThirdActivity: zwm, onResume
2020-10-13 09:37:33.787 24160-24160/com.tomorrow.kotlindemo D/MainActivity: zwm, onResume
2020-10-13 09:37:40.043 24160-24160/com.tomorrow.kotlindemo D/SecondActivity: zwm, onResume

Kotlin 中的 javaClass 表示获取当前实例的 Class 对象,相当于在 Java 中调用 getClass() 方法;

而 Kotlin 中的 SecondActivity::class.java 表示获取 SecondActivity 类的 Class 对象,相当于在 Java 中调用 SecondActivity.class。

4.Kotlin:标准函数和静态方法

标准函数:with、run 和 apply

Kotlin 的标准函数指的是 Standard.kt 文件中定义的函数,任何 Kotlin 代码都可以自由地调用所有的标准函数。

let 标准函数的主要作用是配合 ?. 操作符来进行辅助判空处理。

with 函数接收两个参数:第一个参数可以是一个任意类型的对象,第二个参数是一个 Lambda 表达式。with 函数会在 Lambda 表达式中提供第一个参数对象的上下文,并使用 Lambda 表达式的最后一行代码作为返回值返回。

fun testMethod() {
    val list = listOf("Apple", "Banana", "Pear")
    val result = with(StringBuilder()) {
        append("fruits:\n")
        for(fruit in list) {
            append(fruit).append("\n")
        }
        append("done")
        toString()
    }
    Log.d("MainActivity", "zwm, result: $result")
}

日志打印:
2020-10-13 13:24:56.426 7491-7491/com.tomorrow.kotlindemo D/MainActivity: zwm, result: fruits:
    Apple
    Banana
    Pear
    done

run 函数的用法和使用场景和 with 函数是非常类似的,只是稍微做了一些语法改动而已。首先,run 函数是不能直接调用的,而是一定要调用某个对象的 run 函数才行;其次 run 函数只接收一个 Lambda 参数,并且会在 Lambda 表达式中提供调用对象的上下文。其他方面和 with 函数是一样的,包括也会使用 Lambda 表达式中的最后一行代码作为返回值返回。

fun testMethod() {
    val list = listOf("Apple", "Banana", "Pear")
    val result = StringBuilder().run {
        append("fruits:\n")
        for(fruit in list) {
            append(fruit).append("\n")
        }
        append("done")
        toString()
    }
    Log.d("MainActivity", "zwm, result: $result")
}

日志打印:
2020-10-13 13:35:59.639 22919-22919/com.tomorrow.kotlindemo D/MainActivity: zwm, result: fruits:
    Apple
    Banana
    Pear
    done

apply 函数和 run 函数也是极其类似的,都要在某个对象上调用,并且只接收一个 Lambda 参数,也会在 Lambda 表达式中提供调用对象的上下文,但是 apply 函数无法指定返回值,而是会自动返回调用对象本身。

fun testMethod() {
    val list = listOf("Apple", "Banana", "Pear")
    val result = StringBuilder().apply {
        append("fruits:\n")
        for(fruit in list) {
            append(fruit).append("\n")
        }
        append("done")
    }
    Log.d("MainActivity", "zwm, result: ${result.toString()}")
}

2020-10-13 13:41:00.461 25049-25049/? D/MainActivity: zwm, result: fruits:
    Apple
    Banana
    Pear
    done

应用:

val intent = Intent(this, SecondActivity::class.java).apply {
    putExtra("param1", "data1")
    putExtra("param2", "data2")
}
startActivity(intent)
定义静态方法

静态方法在某些编程语言里面又叫作类方法,指的就是那种不需要创建实例就能调用的方法,所有主流的编程语言都会支持静态方法这个特性。静态方法非常适合用于编写一些工具类的功能,因为工具类通常没有创建实例的必要,基本是全局通用的。但是和绝大多数主流编程语言不同的是,Kotlin 却弱化了静态方法这个概念,像工具类这种功能,在 Kotlin 中就非常推荐使用单例类的方式来实现:

object Util {

    fun doAction() {
        println("do action")
    }
}

Util.doAction()

不过,使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,而如果我们只是希望让类中的某一个方法变成静态方法的调用方式的话,可以使用 companion object:

class Util {

    fun doAction() {
        println("do action")
    }

    companion object {

        fun doAction2() {
            println("do action2")
        }
    }
}

Util().doAction() //Util 类的实例方法
Util.doAction2() //伴生对象的实例方法

使用单例类和 companion object 都只是在语法的形式上模仿了静态方法的调用方式,实际上它们都不是真正的静态方法。因此如果你在 Java 代码中以静态方法的形式去调用的话,你会发现这些方法并不存在。如果你确确实实需要定义真正的静态方法,Kotlin仍然提供了两种实现方式:注解和顶层方法。

如果我们给单例类或 companion object 中的方法加上 @JvmStatic 注解,那么 Kotlin 编译器就会将这些方法编译成真正的静态方法:

class Util {

    fun doAction() {
        println("do action")
    }

    companion object {

        @JvmStatic
        fun doAction2() { //真正的静态方法,在 Kotlin 跟 Java 中都可以调用该方法
            println("do action2")
        }
    }
}

注意,@JvmStatic 注解只能加在单例类或 companion object 中的方法上,如果尝试加在一个普通方法上,会直接提示语法错误。

顶层方法指的是那些没有定义在任何类中的方法,Kotlin 编译器会将所有的顶层方法全部编译成静态方法,因此只要你定义了一个顶层方法,那么它就一定是静态方法。在 Kotlin 中调用:所有的顶层方法都可以在任何位置被直接调用,不用管包名路径,也不用创建实例。在 Java 中调用:假设创建的 Kotlin 文件名叫作 Helper.kt,Kotlin 编译器会自动创建一个叫作 HelperKt 的 Java 类,静态方法就是定义在 HelperKt 类里面的,使用 HelperKt 直接调用就可以了。

三、UI 开发

1.常用控件

AlertDialog
button1.setOnClickListener {
    AlertDialog.Builder(this).apply {
        setTitle("This is Dialog")
        setMessage("Something important.")
        setCancelable(false)
        setPositiveButton("OK") { _, _ ->
        }
        setNegativeButton("Cancel") { _, _ ->
        }
        show()
    }
}

2.布局控件

LinearLayout

两个控件平分空间:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <EditText
        android:id="@+id/input_message"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Type something"/>
    <Button
        android:id="@+id/button1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="button1" />
</LinearLayout>

左边控件占剩余空间:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <EditText
        android:id="@+id/input_message"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Type something"/>
    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button1" />
</LinearLayout>

3.自定义控件

//res/layout/title.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#676767">
    <Button
        android:id="@+id/titleBack"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="5dp"
        android:text="Back"
        android:textColor="#ffffff"/>
    <TextView
        android:id="@+id/titleText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_weight="1"
        android:gravity="center"
        android:text="Text"
        android:textColor="#ffffff"/>
    <Button
        android:id="@+id/titleEdit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="5dp"
        android:text="Edit"
        android:textColor="#ffffff"/>
</LinearLayout>

//TitleLayout.kt
package com.tomorrow.kotlindemo

import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.Toast
import kotlinx.android.synthetic.main.title.view.*

class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {

    init {
        LayoutInflater.from(context).inflate(R.layout.title, this)
        titleBack.setOnClickListener {
            val activity = context as Activity
            activity.finish()
        }
        titleEdit.setOnClickListener {
            Toast.makeText(context, "You clicked Edit button", Toast.LENGTH_SHORT).show()
        }
    }
}

//res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.tomorrow.kotlindemo.TitleLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        supportActionBar?.hide()
    }
}

通过 LayoutInflater 的 from() 方法可以构建出一个 LayoutInflater 对象,然后调用 inflate() 方法就可以动态加载一个布局文件。inflate() 方法接收两个参数:第一个参数是要加载的布局文件的 id,第二个参数是给加载好的布局再添加一个父布局。

4.ListView

//Fruit.kt
package com.tomorrow.kotlindemo

class Fruit(val name: String, val imageId: Int)

//res/layout/fruit_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="60dp">
    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"/>
    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp" />
</LinearLayout>

//FruitAdapter.kt
package com.tomorrow.kotlindemo

import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView

class FruitAdapter(activity: Activity, private val resourceId: Int, data: List<Fruit>) : ArrayAdapter<Fruit>(activity, resourceId, data) {

    inner class ViewHolder(val fruitImage: ImageView, val fruitName: TextView)

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val view: View
        val viewHolder: ViewHolder
        if(convertView == null) {
            view = LayoutInflater.from(context).inflate(resourceId, parent, false)
            val fruitImage = view.findViewById(R.id.fruitImage) as ImageView
            val fruitName = view.findViewById(R.id.fruitName) as TextView
            viewHolder = ViewHolder(fruitImage, fruitName)
            view.tag = viewHolder
        } else {
            view = convertView
            viewHolder = view.tag as ViewHolder
        }

        val fruit = getItem(position)
        if(fruit != null) {
            viewHolder.fruitImage.setImageResource(fruit.imageId)
            viewHolder.fruitName.text = fruit.name
        }
        return view
    }
}

//res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initFruits()
        val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
        listView.adapter = adapter
        listView.setOnItemClickListener { _, _, position, _ ->
            val fruit = fruitList[position]
            Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
        }
    }

    private fun initFruits() {
        repeat(2) {
            fruitList.add(Fruit("Apple", R.drawable.ic_launcher))
            fruitList.add(Fruit("Banana", R.drawable.ic_launcher))
            fruitList.add(Fruit("Orange", R.drawable.ic_launcher))
            fruitList.add(Fruit("Watermelon", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pear", R.drawable.ic_launcher))
            fruitList.add(Fruit("Grape", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pineapple", R.drawable.ic_launcher))
            fruitList.add(Fruit("Strawberry", R.drawable.ic_launcher))
            fruitList.add(Fruit("Cherry", R.drawable.ic_launcher))
            fruitList.add(Fruit("Mango", R.drawable.ic_launcher))
        }
    }
}

在 getView() 方法中,首先使用 LayoutInflater 来为这个子项加载我们传入的布局。LayoutInflater 的 inflate() 方法接收 3 个参数,第三个参数指定成 false,表示只让我们在父布局中声明的 layout 属性生效,但不会为这个 View 添加父布局。因为一旦 View 有了父布局之后,它就不能再添加到 ListView 中了。

需要注意的是,kotlin-android-extensions 插件在 ListView 的适配器中是无法工作的,因此这里我们还是得调用 findViewById() 方法来获取子项布局中控件的实例。kotlin-android-extensions 插件的主要应用场景是在 Activity 以及 Fragment 当中。

repeat 函数是 Kotlin 中另外一个非常常用的标准函数,它允许你传入一个数值 n,然后会把 Lambda 表达式中的内容执行 n 遍。

5.RecyclerView

基本使用:

//build.gradle
implementation 'androidx.recyclerview:recyclerview:1.0.0'

//Fruit.kt
package com.tomorrow.kotlindemo

class Fruit(val name: String, val imageId: Int)

//fruit_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="60dp">
    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"/>
    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp" />
</LinearLayout>

//FruitsAdapter.kt
package com.tomorrow.kotlindemo

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class FruitsAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitsAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
        val fruitName: TextView = view.findViewById(R.id.fruitName)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.fruitImage.setImageResource(fruit.imageId)
        holder.fruitName.text = fruit.name
    }

    override fun getItemCount() = fruitList.size
}

//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initFruits()
        val layoutManager = LinearLayoutManager(this)
        recyclerView.layoutManager = layoutManager
        val adapter = FruitsAdapter(fruitList)
        recyclerView.adapter = adapter
    }

    private fun initFruits() {
        repeat(2) {
            fruitList.add(Fruit("Apple", R.drawable.ic_launcher))
            fruitList.add(Fruit("Banana", R.drawable.ic_launcher))
            fruitList.add(Fruit("Orange", R.drawable.ic_launcher))
            fruitList.add(Fruit("Watermelon", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pear", R.drawable.ic_launcher))
            fruitList.add(Fruit("Grape", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pineapple", R.drawable.ic_launcher))
            fruitList.add(Fruit("Strawberry", R.drawable.ic_launcher))
            fruitList.add(Fruit("Cherry", R.drawable.ic_launcher))
            fruitList.add(Fruit("Mango", R.drawable.ic_launcher))
        }
    }
}

实现横向滚动布局:

//fruit_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="80dp"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginLeft="10dp"/>
    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginLeft="10dp" />
</LinearLayout>

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initFruits()
        val layoutManager = LinearLayoutManager(this)
        layoutManager.orientation = LinearLayoutManager.HORIZONTAL
        recyclerView.layoutManager = layoutManager
        val adapter = FruitsAdapter(fruitList)
        recyclerView.adapter = adapter
    }

    private fun initFruits() {
        repeat(2) {
            fruitList.add(Fruit("Apple", R.drawable.ic_launcher))
            fruitList.add(Fruit("Banana", R.drawable.ic_launcher))
            fruitList.add(Fruit("Orange", R.drawable.ic_launcher))
            fruitList.add(Fruit("Watermelon", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pear", R.drawable.ic_launcher))
            fruitList.add(Fruit("Grape", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pineapple", R.drawable.ic_launcher))
            fruitList.add(Fruit("Strawberry", R.drawable.ic_launcher))
            fruitList.add(Fruit("Cherry", R.drawable.ic_launcher))
            fruitList.add(Fruit("Mango", R.drawable.ic_launcher))
        }
    }
}

实现瀑布流布局:

除了 LinearLayoutManager 之外,RecyclerView 还给我们提供了 GridLayoutManager 和 StaggeredGridLayoutManager 这两种内置的布局排列方式。GridLayoutManager 可以用于实现网格布局,StaggeredGridLayoutManager 可以用于实现瀑布流布局。

//fruit_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp">
    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginLeft="10dp"/>
    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_marginLeft="10dp" />
</LinearLayout>

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initFruits()
        val layoutManager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
        recyclerView.layoutManager = layoutManager
        val adapter = FruitsAdapter(fruitList)
        recyclerView.adapter = adapter
    }

    private fun initFruits() {
        repeat(2) {
            fruitList.add(Fruit("Apple", R.drawable.ic_launcher))
            fruitList.add(Fruit("Banana2222222222222222222222222222222222222222", R.drawable.ic_launcher))
            fruitList.add(Fruit("Orange33333333333333333333", R.drawable.ic_launcher))
            fruitList.add(Fruit("Watermelon444444444444444", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pear555555555555555555555555555555", R.drawable.ic_launcher))
            fruitList.add(Fruit("Grape6666666666", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pineapple7777777777777777777777777777777777777777", R.drawable.ic_launcher))
            fruitList.add(Fruit("Strawberry888888888888888888888888888888888888888888888888888888888888888888888", R.drawable.ic_launcher))
            fruitList.add(Fruit("Cherry999", R.drawable.ic_launcher))
            fruitList.add(Fruit("Mango00000000000000000000", R.drawable.ic_launcher))
        }
    }
}

RecyclerView 的点击事件:

不同于 ListView,RecyclerView 并没有提供类似于 setOnItemClickListener() 这样的注册监听器方法,而是需要我们自己给子项具体的 View 去注册点击事件。这相比于 ListView 来说,实现起来要复杂一些。

//FruitsAdapter.kt
package com.tomorrow.kotlindemo

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView

class FruitsAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitsAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
        val fruitName: TextView = view.findViewById(R.id.fruitName)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        val viewHolder = ViewHolder(view)
        viewHolder.itemView.setOnClickListener {
            val position = viewHolder.adapterPosition
            val fruit = fruitList[position]
            Toast.makeText(parent.context, "You clicked view ${fruit.name}", Toast.LENGTH_SHORT).show()
        }
        viewHolder.fruitImage.setOnClickListener {
            val position = viewHolder.adapterPosition
            val fruit = fruitList[position]
            Toast.makeText(parent.context, "You clicked image ${fruit.name}", Toast.LENGTH_SHORT).show()
        }
        return viewHolder
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.fruitImage.setImageResource(fruit.imageId)
        holder.fruitName.text = fruit.name
    }

    override fun getItemCount() = fruitList.size
}

根据不同的 viewType 创建不同的界面:

//fruit_header.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:id="@+id/fruitTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"/>
</LinearLayout>

//fruit_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="60dp">
    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"/>
    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp" />
</LinearLayout>

//FruitsAdapter.kt
package com.tomorrow.kotlindemo

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView

class FruitsAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    companion object {
        const val TYPE_HEADER = 0
        const val TYPE_FRUIT = 1
    }

    inner class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val fruitTitle: TextView = view.findViewById(R.id.fruitTitle)
    }

    inner class FruitViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
        val fruitName: TextView = view.findViewById(R.id.fruitName)
    }

    override fun getItemViewType(position: Int) = if(position == 0) {
            TYPE_HEADER
        } else {
            TYPE_FRUIT
        }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = if(viewType == TYPE_HEADER) {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_header, parent, false)
        HeaderViewHolder(view)
    } else {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        val viewHolder = FruitViewHolder(view)
        viewHolder.itemView.setOnClickListener {
            val position = viewHolder.adapterPosition
            val fruit = fruitList[position]
            Toast.makeText(parent.context, "You clicked view ${fruit.name}", Toast.LENGTH_SHORT).show()
        }
        viewHolder.fruitImage.setOnClickListener {
            val position = viewHolder.adapterPosition
            val fruit = fruitList[position]
            Toast.makeText(parent.context, "You clicked image ${fruit.name}", Toast.LENGTH_SHORT).show()
        }
        viewHolder
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val fruit = fruitList[position]
        when (holder) {
            is HeaderViewHolder -> holder.fruitTitle.text = "Fruits:"
            is FruitViewHolder -> {
                holder.fruitImage.setImageResource(fruit.imageId)
                holder.fruitName.text = fruit.name
            }
        }
    }

    override fun getItemCount() = fruitList.size
}

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initFruits()
        val layoutManager = LinearLayoutManager(this)
        recyclerView.layoutManager = layoutManager
        val adapter = FruitsAdapter(fruitList)
        recyclerView.adapter = adapter
    }

    private fun initFruits() {
        fruitList.add(Fruit("Header", R.drawable.ic_launcher))
        repeat(2) {
            fruitList.add(Fruit("Apple", R.drawable.ic_launcher))
            fruitList.add(Fruit("Banana", R.drawable.ic_launcher))
            fruitList.add(Fruit("Orange", R.drawable.ic_launcher))
            fruitList.add(Fruit("Watermelon", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pear", R.drawable.ic_launcher))
            fruitList.add(Fruit("Grape", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pineapple", R.drawable.ic_launcher))
            fruitList.add(Fruit("Strawberry", R.drawable.ic_launcher))
            fruitList.add(Fruit("Cherry", R.drawable.ic_launcher))
            fruitList.add(Fruit("Mango", R.drawable.ic_launcher))
        }
    }
}

注意,定义常量的关键字是 const,只有在单例类、companion object 或顶层方法中才可以使用 const 关键字。

适配器的 notifyItemInserted() 方法用于通知列表有新的数据插入,这样新增的一条消息才能够在 RecyclerView 中显示出来。

适配器的 notifyDataSetChanged() 方法会将 RecyclerView 中所有可见的元素全部刷新,这样不管是新增、删除、还是修改元素,界面上都会显示最新的数据,但缺点是效率会相对差一点。

6.Kotlin:延迟初始化和密封类

对变量延迟初始化
class MainActivity : AppCompatActivity() {

    private lateinit var adapter: FruitsAdapter
    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initFruits()
        val layoutManager = LinearLayoutManager(this)
        recyclerView.layoutManager = layoutManager
        adapter = FruitsAdapter(fruitList)
        recyclerView.adapter = adapter

        Handler().postDelayed({
            fruitList.add(1, Fruit("New Added", R.drawable.ic_launcher))
            adapter.notifyItemInserted(1)
        }, 5000)
    }

    private fun initFruits() {
        fruitList.add(Fruit("Header", R.drawable.ic_launcher))
        repeat(2) {
            fruitList.add(Fruit("Apple", R.drawable.ic_launcher))
            fruitList.add(Fruit("Banana", R.drawable.ic_launcher))
            fruitList.add(Fruit("Orange", R.drawable.ic_launcher))
            fruitList.add(Fruit("Watermelon", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pear", R.drawable.ic_launcher))
            fruitList.add(Fruit("Grape", R.drawable.ic_launcher))
            fruitList.add(Fruit("Pineapple", R.drawable.ic_launcher))
            fruitList.add(Fruit("Strawberry", R.drawable.ic_launcher))
            fruitList.add(Fruit("Cherry", R.drawable.ic_launcher))
            fruitList.add(Fruit("Mango", R.drawable.ic_launcher))
        }
    }
}

延迟初始化使用的是 lateinit 关键字,它可以告诉 Kotlin 编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为 null 了。当你对一个全局变量使用了 lateinit 关键字时,请一定要确保它在被任何地方调用之前已经完成了初始化工作,否则 Kotlin 将无法保证程序的安全性。

另外,我们还可以通过代码来判断一个全局变量是否已经完成了初始化,这样在某些时候能够有效地避免重复对某一个变量进行初始化操作:

if(!::adapter.isInitialized) {
    adapter = FruitsAdapter(fruitList)
}
使用密封类优化代码

举个例子:

interface Result
class Success(val msg: String) : Result
class Failure(val error: Exception) : Result

fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> result.error.message
    else -> throw IllegalArgumentException()
}

比较让人讨厌的是,接下来我们不得不再编写一个 else 条件,否则 Kotlin 编译器会认为这里缺少条件分支,代码将无法编译通过。不过 Kotlin 的密封类可以很好地解决这个问题。

sealed class Result
class Success(val msg: String) : Result()
class Failure(val error: Exception) : Result()

fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> result.error.message
}

当在 when 语句中传入一个密封类变量作为条件时,Kotlin 编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应的条件全部处理。这样就可以保证,即使没有编写 else 条件,也不可能会出现漏写条件分支的情况。另外,密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。

四、Fragment

1.Fragment 的使用方式

Fragment 的简单用法
//left_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Button"/>
</LinearLayout>

//right_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#888888">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="24sp"
        android:text="This is right fragment"/>
</LinearLayout>

//LeftFragment.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment

class LeftFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.left_fragment, container, false)
    }
}

//RightFragment.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment

class RightFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.right_fragment, container, false)
    }
}

//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment
        android:id="@+id/leftFragment"
        android:name="com.tomorrow.kotlindemo.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    <fragment
        android:id="@+id/rightFragment"
        android:name="com.tomorrow.kotlindemo.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
</LinearLayout>

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG, "zwm, onCreate")
    }
}
动态添加 Fragment
//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment
        android:id="@+id/leftFragment"
        android:name="com.tomorrow.kotlindemo.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
    <FrameLayout
        android:id="@+id/rightLayout"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
</LinearLayout>

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.left_fragment.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            replaceFragment(RightFragment())
        }
    }

    private fun replaceFragment(fragment: Fragment) {
        val fragmentManager = supportFragmentManager
        val transaction =  fragmentManager.beginTransaction()
        transaction.replace(R.id.rightLayout, fragment)
        transaction.commit()
    }
}
在 Fragment 中实现返回栈
//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.left_fragment.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            replaceFragment(RightFragment())
        }
    }

    private fun replaceFragment(fragment: Fragment) {
        val fragmentManager = supportFragmentManager
        val transaction =  fragmentManager.beginTransaction()
        transaction.replace(R.id.rightLayout, fragment)
        transaction.addToBackStack(null)
        transaction.commit()
    }
}
Fragment 和 Activity 之间的交互

在 Activity 中获取 Fragment 的实例:

val fragment = supportFragmentManager.findFragmentById(R.id.leftFragment) as LeftFragment

另外,类似于 findViewById() 方法,kotlin-android-extensions 插件也对 findFragmentById() 方法进行了扩展,允许我们直接使用布局文件中定义的 Fragment id 名称来自动获取相应的 Fragment 实例(推荐方式):

val fragment = leftFragment as LeftFragment

在 Fragment 中获取 Activity 的实例:

if(activity != null) {
    val mainActivity = activity as MainActivity
}

2.Fragment 的生命周期

Activity 中有的生命周期回调方法,Fragment 中基本上也有,不过 Fragment 还提供了一些附加的回调方法:

  • onAttach():当 Fragment 和 Activity 建立关联时调用。
  • onCreateView():为 Fragment 创建视图(加载布局)时调用。
  • onActivityCreated():确保与 Fragment 相关联的 Activity 已经创建完毕时调用。
  • onDestroyView():当与 Fragment 关联的视图被移除时调用。
  • onDetach():当 Fragment 和 Activity 解除关联时调用。

在 Fragment 中也可以通过 onSaveInstanceState() 方法来保存数据,因为进入停止状态的 Fragment 有可能在系统内存不足的时候被回收。保存下来的数据在 onCreate()、onCreateView() 和 onActivityCreated() 这 3 个方法中都可以重新得到,它们都含有一个 Bundle 类型的 savedInstanceState 参数。

3.Kotlin:扩展函数和运算符重载

扩展函数

扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。定义扩展函数的语法结构:

fun ClassName.methodName(param1: Int, param2: Int): Int {
    return 0
}

相比于定义一个普通的函数,定义扩展函数只需要在函数名的前面加上一个 ClassName. 的语法结构,就表示将该函数添加到指定类当中了。扩展函数可以定义在任何一个现有类当中,建议是,向哪个类中添加扩展函数,就定义一个同名的 Kotlin 文件,把扩展函数定义成顶层方法,这样可以让扩展函数拥有全局的访问域。

//String.kt
package com.tomorrow.kotlindemo

fun String.lettersCount(): Int {
    var count = 0
    for(char in this) {
        if(char.isLetter()) {
            count++
        }
    }
    return count
}

"Hello, Kotlin!".lettersCount()
运算符重载

运算符重载是 Kotlin 提供的一个比较有趣的语法糖。运算符重载使用的是 operator 关键字,只要在指定函数的前面加上 operator 关键字,就可以实现运算符重载功能了。

//Money.kt
package com.tomorrow.kotlindemo

class Money(val value: Int) {

    operator fun plus(money: Money): Money {
        val sum = value + money.value
        return Money(sum)
    }

    operator fun plus(newValue: Int): Money {
        val sum = value + newValue
        return Money(sum)
    }
}

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val money1 = Money(5)
        val money2 = Money(10)
        val money3 = money1 + money2
        val money4 = money3 + 20
        Log.d(TAG, "money: ${money4.value}")
    }
}

日志打印:
2020-10-16 20:38:05.651 3264-3264/com.tomorrow.kotlindemo D/MainActivity: money: 35

语法糖表达式和实际调用函数对照表:

语法糖表达式 实际调用函数
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a++ a.inc()
a-- a.dec()
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
a == b a.equals(b)
a > b a.compareTo(b)
a < b a.compareTo(b)
a >= b a.compareTo(b)
a <= b a.compareTo(b)
a..b a.rangeTo(b)
a[b] a.get(b)
a[b] = c a.set(b, c)
a in b b.contains(a)