第2章 探究新语言,快速入门Kotlin编程
- Kotlin可以做到和Java 100%兼容
- 这主要是得益于Java虚拟机的工作机制。其实Java虚拟机并不会直接和你编写的Java代码打交道,而是和编译之后生成的class文件打交道。而Kotlin也有一个自己的编译器,它可以将Kotlin代码也编译成同样规格的class文件。Java虚拟机不会关心class文件是从Java编译来的,还是从Kotlin编译来的,只要是符合规格的class文件,它都能识别。
变量
-
Kotlin完全抛弃了Java中的基本数据类型,全部使用了对象数据类型。在Java中int是整型变量的关键字,而在Kotlin中Int变成了一个类,它拥有自己的方法和继承结构。
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kZqWOS6H-1590142300347)(E:\Notes\Kotlin\assets\《代码》笔记.assets\1589798994515.png)]
-
val(value的简写的简写)用来声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋值,对应Java中的final变量。
-
var(variable的简写的简写)用来声明一个可变的变量,这种变量在初始赋值之后仍然可以再被重新赋值复制,对应Java中的非final变量。
-
fun main() { val a = 10//不能重新赋值 var b = 5 b = b + 3 println("a = " + a) println("b = " + b) }
函数
fun methodName(param1: Int, param2: Int): Int {
return max(p1,p2)
}
- 当一个函数的函数体中只有一行代码时,可以使用单行代码函数的语法糖
fun methodName(param1: Int, param2: Int) = max(p1,p2)
使用这种写法,可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可。
return关键字也可以省略,等号足以表达返回值的意思。
Kotlin还拥有出色的类型推导机制,可以自动推导出返回值的类型。
if语句
Kotlin中的if语句相比于Java有一个额外的功能:它是可以有返回值的,返回值就是if语句每一个条件中最后一行代码的返回值。
fun largerNumber(num1: Int, num2: Int): Int {
val value = if (num1 > num2) {
num1
} else {
num2
}
return value
}
仔细观察上述代码,你会发现value其实是一个多余的变量,我们可以直接将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语句
- when不需要自己break,switch需要
- switch只支持整型和字符串等类型,when还允许类型匹配,比如is Int的判断
- when和if一样可以有返回值
fun getScore(name: String) = when (name) {
"Tom" -> 86
"Jim" -> 77
"Jack" -> 95
"Lily" -> 100
else -> 0
}
除了精确匹配之外,when语句还允许进行类型匹配。
fun checkNumber(num: Number) {
when (num) {
is Int -> println("number is Int")
is Double -> println("number is Double")
else -> println("number not support")
}
}
for-in循环语句
可以通过for-in循环来遍历这个区间,它的数学表达方式是[0, 10),不包含10,一般创建一个10为大小的数组也不包含10这个下标:
fun main() {
for (i in 0 until 10) {
println(i)
}
}
如果你想跳过其中的一些元素,可以使用step关键字:
fun main() {
for (i in 0 until 10 step 2) {
println(i)
}
}
如果你想创建一个降序的区间,可以使用downTo关键字:
fun main() {
for (i in 10 downTo 1) {
println(i)
}
}
类与对象
class Person {
var name = ""
var age = 0
fun eat() {
println(name + " is eating. He is " + age + " years old.")
}
}
fun main() {
val p = Person()
p.name = "Jack"
p.age = 19
p.eat()
}
- Kotlin中一个类默认是不可以被继承的,如果想要让一个类可以被继承,需要主动声明open关键字:
class Student : Person() {
var sno = ""
var grade = 0
}
- 主构造函数,特点是没有函数体,直接定义在类名后面
open class Person(val name: String, val age: Int) {
fun eat() {
println(name + " is eating. He is " + age + " years old.")
}
}
- 实例化的时候,必须传入主构造函数的所有参数
- 如果现在主构造函数中写逻辑,那么写在init代码块里
- 继承了一个类的子类,继承的时候要传递父类的参数
- 同时如下,不再声明var name和age,因为在父类中声明过了,不能声明同名的变量
class Student(val sno: String = "", val grade: Int = 0, name: String = "", age: Int = 0) : Person(name, age), Study {
override fun readBooks() {
println(name + " is reading.")
}
//init代码块
init {
println(sno)
println(grade)
}
}
- constructor声明去调用副构造函数,但是后面要this去调用主构造函数
class Student(val sno: String = "", val grade: Int = 0, name: String = "", age: Int = 0) : Person(name, age), Study {
//副构造函数,this调用了主构造函数
constructor(name:String,age:Int):this("",0,name,age){
}
//this调用了第一个副构造函数
constructor():this("",0){
}
}
- 当只有副构造函数,没有主构造函数的时候。
- 没有在这里调用父类的主构造函数,所以Person后面没有括号
- 副构造函数,super调用了父类的主构造函数
//没有在这里调用父类的主构造函数,所以Person后面没有括号
class Student : Person {
//副构造函数,super调用了父类的主构造函数
constructor(name:String,age:Int):super(name,age){
}
}
接口
- Kotlin中定义接口的关键字和Java中是相同的,都是使用的interface:
interface Study {
fun readBooks()
fun doHomework()
}
- 而Kotlin中实现接口的关键字变量了冒号,和继承使用的是同样的关键字冒号:
- 但是接口不需要用括号,因为没有构造函数
class Student(val name: String, val age: Int) : Study {
override fun readBooks() {
println(name + " is reading.")
}
override fun doHomework() {
println(name + " is doing homework.")
}
}
- 多态的一种实现:方法的参数类型是接口Study,但是我们可以传入一个Student类型的变量
- Kotlin的接口蕴蓄定义函数的默认实现
- 实现了的方法,可以重写也可以不重写,其他方法没有实现,就一定要重写
- JDK1.8之后也支持
可见性修饰符
- private和Java一样,都是只对当前类内部可见
- public表示对所有类对可见
- Kotlin默认public
- protected
- Java中表示对当前类、子类和同一包下的类可见
- Kotlin则是只对当前类和子类可见
- Kotlin抛弃了Java的defalut可见性(同一包路径下的类可见),而是用internal表示对同一模块的类可见,对于模块开发比较合适
数据类
- Kotlin中使用data关键字可以定义一个数据类:
data class Cellphone(val brand: String, val price: Double)
- Kotlin会根据数据类的主构造函数中的参数将equals()、hashCode()、toString()等固定且无实际逻辑意义的方法自动生成,从而大大简少了开发的工作量。
单例类
- Kotlin中使用object关键字可以定义一个单例类:
object Singleton {
fun singletonTest() {
println("singletonTest is called.")
}
}
- 而调用单例类中的函数比较类似于Java中静态方法的调用方式:
Singleton.singletonTest()
- 这种写法虽然看上去像是静态方法的调用,但其实Kotlin在背后自动帮我们创建了一个Singleton类的实例,并且保证全局只会存在一个Singleton实例。
集合
使用如下代码可以初始化一个List集合:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
遍历list
for (fruit in list) {
println(fruit)
}
使用如下代码可以初始化一个Set集合:
val set = setOf("Apple", "Banana", "Orange", "Pear", "Grape")
使用如下代码可以初始化一个Map集合(to表示kv形式):
val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)
遍历map,取出的是kv形式的一对括号
for ((fruit, number) in map) {
println("fruit is " + fruit + ", number is " + number)
}
- listOf是一个不可变的集合,只能读取,不能添加修改和删除,可变集合是mutableListOf
val list = mutableListOf("Apple", "Banana", "Orange", "Pear", "Grape")
集合的函数式API
- maxBy可以根据it.length长度找到最长的一个元素,而不需要写for来一个个比较
val max=list.maxBy { it.length }
- Lambda结构
- {参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}
- 集合中的map函数是最常用的一种函数式API,它用于将集合中的每个元素都映射成一个另外的值,映射的规则在Lambda表达式中指定,最终生成一个新的集合。
- 比如,这里我们希望让所有的水果名都变成大写模式,就可以这样写:
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val newList = list.map({ fruit: String -> fruit.toUpperCase() })
for (fruit in newList) {
println(fruit)
}
}
-
当Lambda参数是函数的最后一个参数时,可以将Lambda表达式移到函数括号的外面。
-
如果Lambda参数是函数的唯一一个参数的话,还可以将函数的括号省略。
-
由于Kotlin拥有出色的类型推导机制,Lambda表达式中的参数列表其实在大多数情况下也不必声明参数类型。
-
当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替。
因此,Lambda表达式的写法可以进一步简化成如下方式:
val newList = list.map { it.toUpperCase() }
空指针检查
可空类型系统
fun doStudy(study: Study) {
study.readBooks()
study.doHomework()
}
- 这段代码看上去和刚才的Java版本并没有什么区别,但实际上它是没有空指针风险的,因为Kotlin默认所有的参数和变量都不可为空,所以这里传入的Study参数也一定不会为空,可以放心地调用它的任何函数。
- Kotlin提供了另外一套可为空的类型系统,就是在类名的后面加上一个问号。比如,Int表示不可为空的整型,而Int?就表示可为空的整型;String表示不可为空的字符串,而String?就表示可为空的字符串。
- 使用可为空的类型系统时,需要在编译时期就把所有的空指针异常都处理掉才行。
判空辅助工具
?. 操作符表示当对象不为空时正常调用相应的方法,当对象为空时则什么都不做。比如:
if (a != null) {
a.doSomething()
}
这段代码使用?.操作符就可以简化成:
a?.doSomething()
?: 操作符表示如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。比如:
val c = if (a ! = null) {
a
} else {
b
}
这段代码的逻辑使用?:操作符就可以简化成:
val c = a ?: b
- 结合使用?.操作符和let函数也可以对多次重复调用的某个变量统一进行判空处理(而不需要多次.?或者多次if判空):
- let可以对全局变量判空而if不能
- 因为全局变量随时可能在别处被修改
- let可以对全局变量判空而if不能
fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomework()
}
}
字符串内嵌表达式
Kotlin中字符串内嵌表达式的语法规则如下:
"hello, ${obj.name}. nice to meet you!"
当表达式中仅有一个变量的时候,还可以将两边的大括号省略:
"hello, $name. nice to meet you!"
函数的参数默认值
fun printParams(num: Int, str: String = "hello") {
println("num is $num , str is $str")
}
- 这里给printParams()函数的第二个参数设定了一个默认值,这样当调用printParams()函数时,可以选择给第二个参数传值,也可以选择不传,在不传的情况下就会自动使用默认值。
- 默认参数调用的时候可以用kv的形式去指定
- 主构造函数可以指定默认值
class Student(val sno: String = "", val grade: Int = 0, name: String = "", age: Int = 0)
第3章 先从看得到的入手,探究Activity
- Kotlin引入了一个kotlin-android-extensions插件,根据布局文件的id生成变量,不需要findViewById
- 跳转的intent的构建
- 第二个参数获得指定的class
- SecondActivity::class.java等于Java的SecondActivity.java
- 第二个参数获得指定的class
val intent = Intent(this, SecondActivity::class.java)
- Activity意外消耗,可以用onSaveInstanceState保存
- 手机旋转消耗不一定要用onSaveInstanceState方法,可以用ViewMode
- javaClass就是Java中的getClass,javaClass.simpleName就是获取当前类的名字,可以用这个来获取当前Activity的名字(在BaseActivity的onCreate打印出来)
Kotlin标准函数
- 标准函数是Standard.kt文件中定义的函数,任何Kotlin代码都可以只有地调用标准函数
- let这个标准函数主要是配合?.操作符来判空
- with函数接收两个参数:
- 第一个参数可以是一个任意类型的对象,第二个参数是一个Lambda表达式。
- with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回。
val result = with(obj) {
// 这里是obj的上下文
"value" // with函数的返回值
}
例子:StringBuilder拼接list中的字符串
val list= listOf("apple","banana")
val builder=StringBuilder()
builder.append("start")
for (fruit in list) {
builder.append(fruit)
}
builder.append("end")
builder.toString()
使用with函数可以精简为:
val list= listOf("apple","banana")
val result= with(StringBuilder()){
append("start")
for (s in list) {
append(s)
}
append("end")
toString()//返回值
}
- run函数的用法和使用场景其实和with函数是非常类似的,只是稍微做了一些语法改动而已。
- 首先run函数是不能直接调用的,而是一定要调用某个对象的run函数才行;
- 其次run函数只接收一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文。其他方面和with函数是一样的,包括也会使用Lambda表达式中的最后一行代码作为返回值返回。
- run只是把with传入的StringBuilder对象改成了调用StringBuilder的run方法,其他没有区别
val result = obj.run {
// 这里是obj的上下文
"value" // run函数的返回值
}
val result2=StringBuilder().run {
append("start")
for (s in list) {
append(s)
}
append("end")
toString()//返回值
}
- apply函数和run函数也是极其类似的,都是要在某个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。
val result = obj.apply {
// 这里是obj的上下文
}
// result == obj
val result3=StringBuilder().apply{
append("start")
for (s in list) {
append(s)
}
append("end")
}
//因为apply无法指定返回值,result3实际上还是StringBuilder对象,所以在外面调用toString
println(result3.toString())
例子
//用apply构建intent,放入参数
val intent=Intent(this,SecondActivity::class.java).apply {
putExtra("param1","data1")
putExtra("param2","data2")
}
this.startActivity(intent)
定义静态方法
- 单例类内部都是静态方法
- 但是如果只要某一个方法是静态的,其他方法是普通的,就不能用单例类
object Util {
fun doAction1() {
println("do action1")
}
}
- class普通类,其中的companion object会在Util类内部创建一个伴生类,由于Kotlin会保证一个类只有一个半身类,所以可以用类似静态方法的形式去调用它
class Util {
fun doAction1() {
println("do action1")
}
companion object {
fun doAction2() {
println("do action2")
}
}
}
//调用
Util.doAction2()
- 伴生类的方法不是真正的静态方法,如果在Java中调用发现是不存在的,需要给方法加上@JvmStatic
- 这个注解只能加在单例类或者伴生类的方法上,普通方法会报错
companion object {
@JvmStatic//注解
fun doAction2() {
println("do action2")
}
}
- 另一种构造真正的静态方法的方式是,使用顶层函数,也就是并不是定义在任何类中的的方法
- 创建一个Kotlin文件,选择File而不是class。命名叫HelperKt的类,定义一个方法,这就是顶层方法
fun doSomething() {
println("do something")
}
-
在Kotlin中直接调用,无需创建实例
-
doSomething()
-
-
在Java中需要用类名.方法()调用,系统编译时候会生成一个叫HelperKt的Java类
-
HelperKt.doSomething()
-
第4章 软件也要拼脸蛋,UI开发的点点滴滴
- ListView中还是需要手动FindViewById,Activity和Fragment才不需要
- repeat(n)是另一个标准函数,重复n次,一般可以用来填充数据使用
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.apple_pic))
fruitList.add(Fruit("Banana", R.drawable.banana_pic))
fruitList.add(Fruit("Orange", R.drawable.orange_pic))
fruitList.add(Fruit("Watermelon", R.drawable.watermelon_pic))
fruitList.add(Fruit("Mango", R.drawable.mango_pic))
}
- 没有用到的参数可以用下划线来占位
- 下面的例子实际只用到了position一个变量
listView.setOnItemClickListener { _, _, position, _ ->
val fruit = fruitList[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}
对变量延迟初始化
- 如果使用可空类型,那么声明处要先赋值null,每个调用的地方也要用?.才能调用
- 延迟初始化使用的是lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为null了。
- 调用处也不需要?.
class MainActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var adapter: MsgAdapter
override fun onCreate(savedInstanceState: Bundle?) {
…
adapter = MsgAdapter(msgList)
…
}
override fun onClick(v: View?) {
…
adapter.notifyItemInserted(msgList.size - 1)
…
}
}
- 但是如果没有初始化就调用,也会出现异常
- 所以使用的使用要先判断,取反,如果没有初始化那就先初始化
if (!::adapter.isInitialized) {
adapter = MsgAdapter(msgList)
}
使用密封类优化代码
- 当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应的条件全部处理。
- 这样就可以保证,即使没有编写else条件,也不可能会出现漏写条件分支的情况。
- 好处就是不需要写when的else情况了
- 这里暂时不理解为什么不用枚举呢?
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 -> "Error is ${result.error.message}"
}
第5章 手机平板要兼顾,探究Fragment
扩展函数
- 扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。
fun ClassName.methodName(param1: Int, param2: Int): Int {
return 0
}
- 创建一个String.kt的文件(File类型而不是class)
- 虽然没有文件命名要求,但是建议向哪个类添加扩招函数,就定义同名文件,方便以后查找
- 定义一个String类的扩展方法,可以计算String中的字母数量
fun String.lettersCount(): Int {
var count = 0
for (char in this) {
if (char.isLetter()) {
count++
}
}
return count
}
调用处就有了String实例上下文,调用方式:
"abvc".lettersCount()
运算符重载
Kotlin的运算符重载允许我们让任意两个对象进行相加,或者是进行更多其他的运算操作。
这里以加号运算符为例,如果想要实现让两个对象相加的功能,那么它的语法结构如下:
- operator关键字
- plus对应+号
class Obj {
operator fun plus(obj: Obj): Obj {
// 处理相加的逻辑
}
}
- 如下,定义有Money的plus方法,就能实现两个Money对象相加,还有一个是和int相加的
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)
}
}
- a in b实际调用了b.contains(a),顺序相反
- 乘法*是a.times(b)
- 加法+是a.plus(b)
第6章 全局大喇叭,详解广播机制
定义高阶函数
- 如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。
fun example(func: (String, Int) -> Unit) {
func("hello", 123)
}
可以看到,这里的example()函数接收了一个函数类型的参数,因此example()函数就是一个高阶函数。而调用一个函数类型的参数,它的语法类似于调用一个普通的函数,只需要在参数名的后面加上一对括号,并在括号中传入必要的参数即可。
- 例子:参数两个是int,传入一个操作函数
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
- 传入的这个函数可以用函数引用的写法::fun
val result0= num1AndNum2(num1,num2,::plus)
fun plus(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minus(num1: Int, num2: Int): Int {
return num1 - num2
}
- 可以用Lambda表达式来作为高阶函数调用
val result1 = num1AndNum2(num1, num2) { n1, n2 ->
n1 + n2
}
- 下面的例子中,参数是StringBuilder.()类型,是ClassName.()
- 这样的写法,说明了函数定义在StringBuilder这个类中
- 好处是调用的时候拥有StringBuilder作为上下文
- apply函数的原理是类似的
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this//返回了StringBuilder对象
}
val result = StringBuilder().build {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())
内联函数的作用
- Lambda表达式在编译后转换成了匿名类的实现方式,也就是每次调用都会创建一个匿名类实例,会造成性能开销
- 定义内联函数很简单,只需要在定义高阶函数时加上inline关键字的声明
inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
- 调用的时候,调用处就会替换为内联函数的逻辑代码了,优化性能
noinline与crossinline
- 如果指向对其中一部分Lambda内联,那么不想要内联的可以声明noinline
//第二次参数函数是noinline
inline fun inlineTest(block1: () -> Unit,noinline block2: () -> Unit){
}
-
为什么要有noinline呢?因为inline有局限性
- 内联函数只能传递给另一个内联函数
- 内联函数的Lambda表达式可以return,而非内联函数只能局部返回
- 非内联要 return@方法名来停止
- 内联函数的return 会导致main函数也停止,因为内联函数是替代了代码,return就会停止外层main函数
-
下面这段代码会报错
- 原理暂时没搞懂,和return 有关
inline fun runRunnable(block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EdBbB3hB-1590142300353)(E:\Notes\Kotlin\assets\《代码》笔记.assets\1589987393629.png)]
- 加上crossinline,表示内联函数的Lambda一定不会使用return关键字
- 但是可以用return@runRunnable局部返回
inline fun runRunnable(crossinline block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}
第7章 数据存储全方案,详解持久化技术
文件存储
Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。
所有的文件会默认存储到/data/data//files/目录下。示例写法如下:
- 注意,这里使用到一个扩展函数use,它保证结束后自动关闭外层的流
- 不要再写finally关闭流
fun save(inputText: String) {
try {
val output = openFileOutput("data", Context.MODE_PRIVATE)
val writer = BufferedWriter(OutputStreamWriter(output))
writer.use {//自动关闭流
it.write(inputText)
}
} catch (e: IOException) {
e.printStackTrace()
}
}
Context类中还提供了一个openFileInput()方法,用于从文件中读取数据。
它会自动到/data/data//files/目录下加载文件,并返回一个FileInputStream对象,得到这个对象之后,再通过流的方式就可以将数据读取出来了。示例写法如下:
- 这里使用到了forEachLine ,也是扩展函数,读到的每行内容就回调到Lambda表达式,所以在Lambda表达式中拼接字符串就可以
fun load(): String {
val content = StringBuilder()
try {
val input = openFileInput("data")
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {//读到的每行内容就回调到Lambda表达式,所以在Lambda表达式中拼接字符串就可以
content.append(it)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
return content.toString()
}
SharedPreferences存储
- 向SharedPreferences文件中存储数据了,主要可以分为3步实现。
- 调用SharedPreferences对象的edit()方法获取一个SharedPreferences.Editor对象。
- 向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法,添加一个字符串则使用putString()方法,以此类推。
- 调用apply()方法将添加的数据提交,从而完成数据存储操作。
- Android官方的KTX库有高阶函数的简便写法
getSharedPreferences("data", Context.MODE_PRIVATE).edit {
putString("name","Tom")
putInt("age",28)
}
SharedPreferences对象中提供了一系列的get方法,用于对存储的数据进行读取,每种get方法都对应了SharedPreferences.Editor中的一种put方法。
比如读取一个布尔型数据就使用getBoolean()方法,读取一个字符串就使用getString()方法。
示例写法如下:
val prefs = getSharedPreferences("data", Context.MODE_PRIVATE)
val name = prefs.getString("name", "")
val age = prefs.getInt("age", 0)
val married = prefs.getBoolean("married", false)
数据库存储
Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类,借助这个类可以非常简单地对数据库进行创建和升级。示例写法如下:
- onCreate方法:在外部要getHelper的时候会传入数据库名称,
- 如果这个数据库存在,就不会调用onCreate
- 不存在,则调用到onCreate
class MyDatabaseHelper(val context: Context, name: String, version: Int) : SQLiteOpenHelper(context, name, null, version) {
private val createBook = "create table Book (" +
"id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)
Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
}
}
- onUpgrade()方法是用于对数据库进行升级的。示例写法如下:
- 外部要调用onUpgrade()方法,需要传入版本号不同,比上次的大
- 但是这种做法会删除旧的数据库?数据怎么办?
class MyDatabaseHelper(val context: Context, name: String, version: Int) : SQLiteOpenHelper(context, name, null, version) {
…
private val createCategory = "create table Category (" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)
db.execSQL(createCategory)
Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("drop table if exists Book")
db.execSQL("drop table if exists Category")
onCreate(db)
}
}
添加数据
SQLiteDatabase中提供了一个insert()方法,专门用于添加数据。它接收3个参数:
- 第一个参数是表名
- 第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般我们用不到这个功能,直接传入null即可;
- 第三个参数是一个ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。
示例写法如下:
- 因为id是自增长的,所以没有填充也是可以的
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
val db = dbHelper.writableDatabase
val values1 = ContentValues().apply {
// 开始组装第一条数据
put("name", "The Da Vinci Code")
put("author", "Dan Brown")
put("pages", 454)
put("price", 16.96)
}
db.insert("Book", null, values1) // 插入一条数据
更新数据
SQLiteDatabase中提供了一个非常好用的update()方法,用于对数据进行更新。
- 这个方法接收4个参数:第一个参数和insert()方法一样,也是表名,指定更新哪张表里的数据;
- 第二个参数是ContentValues对象,要把更新数据在这里组装进去;
- 第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认会更新所有行。
- 第三个参数是where部门,表示更新name等于?的行,?是占位符
- 第四个参数是提供第三个参数的?的内容,需要传入一个字符串数组
- 示例用的arrayOf是一个创建字符串数组的做法,虽然这里只有一个元素,也要用数组
示例写法如下:
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
val db = dbHelper.writableDatabase
val values = ContentValues()
values.put("price", 10.99)
db.update("Book", values, "name = ?", arrayOf("The Da Vinci Code"))
删除数据
SQLiteDatabase中提供了一个delete()方法,专门用于删除数据。
- 这个方法接收3个参数:第一个参数仍然是表名,
- 第二、第三个参数用于约束删除某一行或某几行的数据,不指定的话默认会删除所有行。
- 和更新操作的三四参不多
示例写法如下:
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
val db = dbHelper.writableDatabase
db.delete("Book", "pages > ?", arrayOf("500"))
查询数据
SQLiteDatabase中还提供了一个query()方法用于对数据进行查询。这个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。参数的详细解释见下表:
query()方法参数 | 对应SQL部分 | 描 述 |
---|---|---|
table | from table_name | 指定查询的表名 |
columns | select column1, column2 | 指定查询的列名 |
selection | where column = value | 指定where的约束条件 |
selectionArgs | - | 为where中的占位符提供具体的值 |
groupBy | group by column | 指定需要group by的列 |
having | having column = value | 对group by后的结果进一步约束 |
orderBy | order by column1, column2 | 指定查询结果的排序方式 |
查询Book表中所有数据的示例写法:
- 会返回一个cursor对象,循环获取数据
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
val db = dbHelper.writableDatabase
// 查询Book表中所有的数据
val cursor = db.query("Book", null, null, null, null, null, null)
if (cursor.moveToFirst()) {
do {
// 遍历Cursor对象,取出数据并打印
val name = cursor.getString(cursor.getColumnIndex("name"))
val author = cursor.getString(cursor.getColumnIndex("author"))
val pages = cursor.getInt(cursor.getColumnIndex("pages"))
val price = cursor.getDouble(cursor.getColumnIndex("price"))
Log.d("MainActivity", "book name is $name")
Log.d("MainActivity", "book author is $author")
Log.d("MainActivity", "book pages is $pages")
Log.d("MainActivity", "book price is $price")
} while (cursor.moveToNext())
}
cursor.close()
使用SQL操作数据库
添加数据的方法如下:
db.execSQL(
“insert into Book (name, author, pages, price) values(?, ?, ?, ?)”, arrayOf("The Da Vinci Code", "Dan Brown", "454", "16.96")
)
更新数据的方法如下:
db.execSQL("update Book set price = ? where name = ?", arrayOf("10.99", "The Da Vinci Code"))
删除数据的方法如下:
db.execSQL("delete from Book where pages > ?", arrayOf("500"))
查询数据的方法如下:
val cursor = db.rawQuery("select * from Book", null)
- 除了查询调用是rawQuery其他都是execSQL方法
使用事务
- 例子是先删除book表中的数据,再insert新的数据
- db.beginTransaction() // 开启事务。执行完成就 db.setTransactionSuccessful() // 事务已经执行成功。最后在finally里面 db.endTransaction() // 结束事务
- 如果注释去掉,抛出一个异常,那么事务就会回滚,不会完成一半删除而没有插入
replaceData.setOnClickListener {
val db = dbHelper.writableDatabase
db.beginTransaction() // 开启事务
try {
db.delete("Book", null, null)
// if (true) {
// // 在这里手动抛出一个异常,让事务失败
// throw NullPointerException()
// }
val values = cvOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720, "price" to 20.85)
db.insert("Book", null, values)
db.setTransactionSuccessful() // 事务已经执行成功
} catch (e: Exception) {
e.printStackTrace()
} finally {
db.endTransaction() // 结束事务
}
}
Kotlin高阶函数的应用
简化SharedPreferences的用法
我们可以使用高阶函数简化SharedPreferences的用法,如下所示:
- open函数拥有SP的上下文,可以直接调用edit()来获得Editor对象
fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}
定义好了open函数之后,以后在项目中使用SharedPreferences存储数据就会更加方便了,写法如下:
getSharedPreferences("data", Context.MODE_PRIVATE).open {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}
Android官方的KTX库中就有一个自带的函数
getSharedPreferences("data", Context.MODE_PRIVATE).edit {
putString("name","Tom")
putInt("age",28)
}
简化ContentValues的用法
-
新建一个ContentValues.kt文件,定义cvOf方法
-
vararg是可变参数
-
Pair的kv形式
- 第二个是Any?相当于object,?表示允许空值
-
for循环读取,最后返回ContentValues对象
-
运用了Kotlin的自动转换,进入when的某个分支,比如Int,那么value就会转换成Int类型
fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
val cv = ContentValues()
for (pair in pairs) {
val key = pair.first
val value = pair.second
when (value) {
is Int -> cv.put(key, value)
is Long -> cv.put(key, value)
is Short -> cv.put(key, value)
is Float -> cv.put(key, value)
is Double -> cv.put(key, value)
is Boolean -> cv.put(key, value)
is String -> cv.put(key, value)
is Byte -> cv.put(key, value)
is ByteArray -> cv.put(key, value)
null -> cv.putNull(key)
}
}
return cv
}
然后就可以使用如下的写法来简单创建ContentValues对象了:
- kv的写法,key to value传入,因为是可变参数,可以传入多个
val values = cvOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720, "price" to 20.85)
db.insert("Book", null, values)
使用高阶函数进一步优化,借助apply函数
- apply自动拥有ContentValues的上下文,直接调用,而不需要创建一个
fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
for (pair in pairs) {
val key = pair.first
val value = pair.second
when (value) {
is Int -> put(key, value)
is Long -> put(key, value)
is Short -> put(key, value)
is Float -> put(key, value)
is Double -> put(key, value)
is Boolean -> put(key, value)
is String -> put(key, value)
is Byte -> put(key, value)
is ByteArray -> put(key, value)
null -> putNull(key)
}
}
}
KTX库中也有一个类似的库
- contentValuesOf
val values= contentValuesOf("name" to "this is name","age" to 28)
db.insert("Book",null,values)
第8章 跨程序共享数据,探究ContentProvider
泛型的基本用法
- 如果要定义一个泛型类,就可以这么写:
class MyClass<T> {
fun method(param: T): T {
return param
}
}
在调用MyClass类和method()方法的时候,可以将泛型指定成具体的类型,如下所示:
val myClass = MyClass<Int>()
val result = myClass.method(123)
- 如果不想定义一个泛型类,只是想定义一个泛型方法,只需要将定义泛型的语法结构写在方法上面就可以了,如下所示:
class MyClass {
fun <T> method(param: T): T {
return param
}
}
此时的调用方式也需要进行相应的调整:
val myClass = MyClass()
val result = myClass.method<Int>(123)
可以看到,现在是在调用method()方法的时候指定泛型类型了。
另外,Kotlin还拥有非常出色的类型推导机制,例如传入了一个Int类型的参数,它能够自动推导出泛型的类型就是Int型,因此这里也可以直接省略泛型的指定:
val myClass = MyClass()
val result = myClass.method(123)
- Kotlin还运行我们设置限制,比如上界设置为Number
class MyClass {
fun <T : Number> method(param: T): T {
return param
}
}
-
默认泛型上界是Any?,为可空类型,如果你先设置不可空,那么修改上界为Any
-
6.5.1小节学习了高阶函数,写了StringBuilder.build的例子,这里可以使用泛型,它实现了和apply函数一样的功能
fun <T> T.build(block:T.()->Unit):T{
block()
return this
}
类委托和委托属性
类委托的核心思想在于将一个类的具体实现委托给另一个类去完成
但是委托也有一定的弊端,如果接口中的待实现方法比较少还好,要是有几十甚至上百个方法的话,每个都去这样调用辅助对象中的相应方法实现,写起了就非常复杂了。这个问题在Kotlin中可以通过类委托的功能来解决。
- by关键字实现类委托
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
fun helloWorld() = println("Hello World")
override fun isEmpty() = false
}
现在MySet就成为了一个全新的数据结构类,它不仅永远不会为空,而且还能打印helloWorld(),至于其他Set接口中的功能,则和HashSet保持一致。这就是Kotlin的类委托所能实现的功能。
委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。
我们看一下委托属性的语法结构,如下所示:
这里使用by关键字连接了左边的p属性和右边的Delegate实例,这种写法就代表着将p属性的具体实现委托给了的Delegate类去完成。这里使用by关键字连接了左边的p属性和右边的Delegate实例,这种写法就代表着将p属性的具体实现委托给了的Delegate类去完成。当调用p属性的时候会自动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的setValue()方法。
class MyClass {
var p by Delegate()
}
因此,我们还得对Delegate类进行具体的实现才行,代码如下所示:
- getValue和 setValue都要用operator修饰
整个委托属性的工作流程就是这样实现的,现在当我们给MyClass的p属性赋值时,就会调用Delegate类的setValue()方法,当获取MyClass中p属性的值时,就会调用Delegate类的getValue()方法。
- getValue
- 第一个参数类型是MyClass,表示只能在MyClass类中使用
- 第二个参数KProperty<*>是Kotlin中的一个属性操作类,可以用于获取各种属性相关的值
- <*>表示你不知道或者不关心泛型的具体类型,类似于Java的<?>
- setValue
- 前两个参数和上面一样
- 最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和get方法一致(这里都是Any?)
class Delegate {
var propValue: Any? = null
operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
return propValue
}
operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
propValue = value
}
}
实现一个自己的lazy函数
- by lazy只有by是关键字,lazy是一个高阶函数。在lazy函数中会创建并返回一个Delegate对象,当我们调用p属性的时候,其实调用的是Delegate对象的getValue方法,然后getValue方法中会调用lazy函数传入的Lambda表达式
- 新建一个Later.kt文件
- 使用一个Any?类型的变量value缓存,如果value为null才去调用block的Lambda表达式去赋值,否则就直接return value强转成为T
- 最后定义一个函数,可以被外界调用
class Later<T>(val block: () -> T) {
var value: Any? = null
operator fun getValue(any: Any?, prop: KProperty<*>): T {
if (value == null) {
value = block()
}
return value as T
}
}
//暴露给外界的函数
fun <T> later(block: () -> T) = Later(block)
调用处,使用by later关键字,并且传入Lambda表达式
private val uriMatcher by later {
val matcher = UriMatcher(UriMatcher.NO_MATCH)
matcher.addURI(authority, "book", bookDir)
matcher.addURI(authority, "book/#", bookItem)
matcher.addURI(authority, "category", categoryDir)
matcher.addURI(authority, "category/#", categoryItem)
matcher
}
第9章 丰富你的程序,运用手机多媒体
- Android 8.0系统引入了通知渠道这个概念
- 每条通知都要属于一个对应的渠道。每个应用程序都可以自由地创建当前应用拥有哪些通知渠道,但是这些通知渠道的控制权是掌握在用户手上的。用户可以自由地选择这些通知渠道的重要程度,是否响铃、是否振动或者是否要关闭这个渠道的通知。
- 比如推特App就区分了通知的渠道有私信、新闻、关注者的消息等
使用infix函数构建更可读的语法
借助infix函数,我们可以使用一种更具可读性的语法来表达一段代码。
infix fun String.beginsWith(prefix: String) = startsWith(prefix)
这里给String类添加了一个beginsWith()函数,它用于判断一个字符串是否是以某个指定参数开头。而加上了infix关键字之后,beginsWith()函数就变成了一个infix函数,这样除了传统的函数调用方式之外,我们还可以用一种特殊的语法糖格式调用beginsWith()函数,如下所示:
if ("Hello Kotlin" beginsWith "Hello") {
// 处理具体的逻辑
}
- “key” to “value” 实际上也是infix函数,包装成为一个Pair对象
- 我们可以写一个类似的with函数
infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)
- has函数判断一个元素是否在一个集合中,也是infix函数
infix fun <T> Collection<T>.has(element: T) = contains(element)
第10章 后台默默的劳动者,探究Service
class MyThread : Thread() {
override fun run() {
// 编写具体的逻辑
}
}
MyThread().start()
- 直接用Lambda表达式
Thread{
//逻辑
}.start()
Kotlin还给我们提供了一种更加简单的开启线程的方式,写法如下:
- thread是内置的顶层函数
thread {
// 编写具体的逻辑
}
泛型实化
-
Kotlin可以把内联函数中的泛型实化
- 因为内联函数就是替代了原来的函数
-
在内联函数的泛型指定了reified,调用这个函数可以获得泛型的类型
- 这个是Java不能做到的
inline fun <reified T> getGenericType() = T::class.java
fun main() {
val result1 = getGenericType<String>()
val result2 = getGenericType<Int>()
//打印出了result1 is class java.lang.String
//result2 is class java.lang.Integer
println("result1 is $result1")
println("result2 is $result2")
}
泛型实化的应用
- 之前每次跳转都需要传入xxActivity::class.java
定义如下方法
inline fun <reified T> startActivity(context: Context) {
val intent = Intent(context, T::class.java)
context.startActivity(intent)
}
调用处
startActivity<MainActivity>(this)
如果有需要传入数据的跳转,则需要一个Lambda
inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
val intent = Intent(context, T::class.java)
intent.block()
context.startActivity(intent)
}
调用处
startActivity<MainActivity>(this){
putExtra("param1","data")
}
泛型的协变
一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为in位置,而它的返回值是输出数据的地方,因此可以称它为out位置,如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j9L90Cf5-1590142300356)(E:\Notes\Kotlin\assets\《代码》笔记.assets\1590047542291.png)]
- 协变的定义:假如定义了一个MyClass的泛型类,其中A是B的子类型,同时MyClass又是MyClass的子类型,那么我们就可以称MyClass在T这个泛型上是协变的。
- 使用了协变,就是可读不可以添加的,不能在函数参数出现泛型,只能在返回值出现泛型
- out关键字
- List是只读的,所以天然就是可以协变的,事实上源码的泛型就是
- 但是contains的参数出现泛型,因为contains传入但是实际上不会修改,所以使用了@UnsafeVariance注解,编译器就允许泛型E出现在in位置上
- 因为List使用了协变,Kotlin就允许List接收List,Java不允许
- 逆变的定义:假如定义了一个MyClass的泛型类,其中A是B的子类型,同时MyClass又是MyClass的子类型,那么我们就可以称MyClass在T这个泛型上是逆变的。
- 用了逆变,就是可以添加不可以读,能在函数参数出现泛型,不能在返回值出现泛型
- in关键字
- 逆变不是特别理解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QMkDjvJh-1590142300360)(E:\Notes\Kotlin\assets\《代码》笔记.assets\1590047723717.png)]
- 我们在泛型的声明前面加上了一个out关键字。这就意味着现在T只能出现在out位置上,而不能出现在in位置上,同T时也意味着SimpleData在泛型T上是协变的。
class SimpleData<out T>(val data: T?) {
fun get(): T? {
return data
}
}
- 我们在泛型T的声明前面加上了一个in关键字。这就意味着现在T只能出现在in位置上,而不能出现在out位置上,同时也意味着Transformer在泛型T上是逆变的。
interface Transformer<in T> {
fun transform(t: T): String
}
第11章 看看精彩的世界,使用网络技术
- 单例的ServiceCreator,创建出retrofitClient
- 暴露一个对外的内联函数,使用了函数实化,只需要传入类型名就可以动态获取类型
object ServiceCreator {
private const val BASE_URL = "http://10.0.2.2/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
inline fun <reified T> create(): T = create(T::class.java)
}
调用
val appService = ServiceCreator.create<AppService>()
协程的概念
协程和线程是有点类似的,可以简单地将它理解成一种轻量级的线程。
要知道,我们之前所学习的线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换。而使用协程却可以仅在编程语言的层面就能实现不同协程之间的切换,从而大大提升了并发编程的运行效率。
协程的基本用法
Kotlin并没有将协程纳入标准库的API当中,而是以依赖库的形式提供的。所以如果我们想要使用协程功能,需要先在app/build.gradle文件当中添加如下依赖库:
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
}
启动协程最简单的方式就是使用Global.launch函数,如下所示:
- GlobalScope.launch函数可以创建一个协程的作用域,这样传递给launch函数的代码块(Lambda表达式)就是在协程中运行的了。
- 使用Thread.sleep,否则可能main函数太快结束,导致日志打印不出来
- delay是非阻塞的挂起函数,不会影响其他协程。Thread.sleep()会阻塞当前线程,也就阻塞当前线程下的所有协程
fun main() {
GlobalScope.launch {
delay(1500)
println("codes run in coroutine scope")
}
Thread.sleep(2000)
}
- runBlocking函数也可以用于启动一个协程,并且会保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。
- 正式环境中使用会产生性能问题
fun main() {
runBlocking {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
}
多协程
使用launch函数可以用于创建多个协程,如下所示:
- launch函数必须在协程的作用域中才能调用,它会在当前协程的作用域下创建子协程
- 子协程的特点是,如果外层作用域的协程结束了,那么子协程也会结束
- GlobalScope.lauch函数创建的永远是顶层协程
- 线程没有层级一说,都是顶层的
fun main() {
runBlocking {
launch {
println("launch1")
delay(1000)
println("launch1 finished")
}
launch {
println("launch2")
delay(1000)
println("launch2 finished")
}
}
}
-
lauch函数的逻辑代码如果提取到单独的函数,就没有协程作用域了。那么如果调用像是delay这种挂起函数呢?
-
Kotlin提供了suspend关键字,使用它可以把函数声明成挂起函数,挂起函数之间都是可以互相调用的
suspend fun printDot() {
println(".")
delay(1000)
}
suspend关键字只能声明函数成为挂起函数,无法提供协程作用域
- 如果你尝试在printDot函数调用launch函数,是无法成功的
- 这个问题用coroutineScope函数来解决
- coroutineScope也是一个挂起函数,因此可以在任何其他挂起函数中调用
- 它会继承外部的协程作用域,并且创建一个子作用域
- coroutineScope和runBlocking有点类似,保证其作用域内的所有代码和子协程都全部执行完之前,会一直阻塞当前协程
- 但是coroutineScope函数只会阻塞当前协程,不影响其他协程,也不影响任何线程,不会有性能问题
- runBlocking函数由于会阻塞当前线程,如果在主线程调用,会导致卡死
suspend fun printDot2()= coroutineScope {
launch {
println(".")
delay(1200)
}
}
更多的作用域构建器
常用的写法:使用job来取消协程
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
// do something
}
job.cancel()//记得取消
- 如果要获取执行结果,使用async函数
- async函数必须在协程作用域才能调用,会创建一个新的子协程并返回一个Deferred对象,调用Deferred的await方法获取运算结果
- 调用async,代码块的代码会立刻执行,当调用await方法时,如果代码块中的代码还没执行完,那么会把协程阻塞住,直到返回结果
- 每次async都在后面调用await会非常低效,使得串行执行,在需要调用到返回结果才调用await方法,会高效一些,如下,打印的时候才调用
runBlocking {
// val start = System.currentTimeMillis()
// val deferred1 = async {
// delay(1000)
// 5 + 5
// }
// val deferred2 = async {
// delay(1000)
// 4 + 6
// }
// println("result is ${deferred1.await() + deferred2.await()}.")
// val end = System.currentTimeMillis()
// println("cost ${end - start} milliseconds.")
// }
- withContext是一个挂起函数,大题可以理解成async的简化版写法
- 以下相当于执行完成等待结果return,也就是async+await
- 参数强制要求指定线程
- Default,适用于低并发,如果逻辑属于计算密集型任务,过高的线程会影响效率
- IO,表示高并发策略,比如网络请求
- Main,不会开启子线程
- 其他的函数都可以指定线程参数,知识withContext是强制要求
runBlocking {
val result= withContext(Dispatchers.Default){
5+5
}
println(result)
}
使用协程简化回调写法
- suspendCoroutine函数必须在协程作用域或者挂起函数中调用,接受一个Lambda雕大师,主要作用是将当前协程立即挂起,然后再一个普通的线程中执行Lambda代码。Lambda的参数列表上回传入一个Continuation参数,调用它的resume方法或者resumeWithException可以让协程恢复执行
okhttp的协程写法
- suspendCoroutine函数协程会被立刻挂起,Lambda在普通线程执行,最后回调给resume,如果是错误就resumeWithException
suspend fun getBaiduResponse() {
try {
val response = request("https://www.baidu.com/")
// 得到服务器返回的具体内容
println(response)
} catch (e: Exception) {
// 在这里对异常情况进行处理
}
}
suspend fun request(address: String): String {
return suspendCoroutine { continuation ->
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
continuation.resume(response)
}
override fun onError(e: Exception) {
continuation.resumeWithException(e)
}
})
}
}
retrofit的协程写法
- 因为有不同接口,使用了泛型。
- await函数定义成了Call的扩展函数,这样所有返回值是Call类型的Retrofit请求接口都可以直接调用这个函数
- 因为有上下文,所以直接调用enqueue方法
suspend fun <T> Call<T>.await(): T {
return suspendCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null) continuation.resume(body)
else continuation.resumeWithException(RuntimeException("response body is null"))
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
suspend fun getAppData() {
try {
val appList = ServiceCreator.create<AppService>().getAppData().await() // 这段代码想运行通过,需要将BASE_URL中的地址改成http://localhost/
println(appList)
// 对服务器响应的数据进行处理
} catch (e: Exception) {
// 对异常情况进行处理
e.printStackTrace()
}
}
第12章 最佳的UI体验,Material Design实战
求N个数的最大值和最小值
- 使用可变参数vararg,传入多个数字也可以进行比较
- 但是只限制了Int类型
fun max(vararg nums: Int): Int {
if (nums.isEmpty()) throw RuntimeException("Params can not be empty.")
var maxNum = nums[0]
for (num in nums) {
if (num > maxNum) {
maxNum = num
}
}
return maxNum
}
- 使用泛型,上界是Comparable,参数和返回类型都是T。这样就可以使用浮点型、整型、长整型等类型了
fun <T : Comparable<T>> max(vararg nums: T): T {
if (nums.isEmpty()) throw RuntimeException("Params can not be empty.")
var maxNum = nums[0]
for (num in nums) {
if (num > maxNum) {
maxNum = num
}
}
return maxNum
}
简化Toast的用法
- 使用扩展函数的写法,调用处直接用String调用,甚至是R资源
- 使用默认参数默认时长是短时间,如果调用者要显示长时间,也可以传入
fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(context, this, duration).show()
}
fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(context, this, duration).show()
}
调用处
"Data restored".showToast(this)//默认显示时间短
R.string.app_name.showToast(this)
"Data restored".showToast(this,Toast.LENGTH_LONG)//可以更改时长,而不是使用默认时间
简化Snackbar用法
- 普通的Snackbar第一个参数是view,所以这里使用了view扩展函数
- SnackBar的特点是有action按钮可以操作,所以参数有一个高阶函数
- 使用的时候传入一个Lambda
- Lambda表达式是按钮点击之后显示的内容和逻辑
- 使用的时候传入一个Lambda
fun View.showSnackbar(text: String, actionText: String? = null, duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? = null) {
val snackbar = Snackbar.make(this, text, duration)
if (actionText != null && block != null) {
snackbar.setAction(actionText) {
block()
}
}
snackbar.show()
}
调用
view.showSnackbar("Data deleted", "Undo") {//第二个参数是action按钮的名字,Lambda表达式是按钮点击之后显示的内容
"Data restored".showToast(this)
}
第13章 高级程序开发组件,探究Jetpack
- Jetpack是一个开发组件工具集,它的主要目的是帮助我们编写出更加简洁的代码,并简化我们的开发过程。
ViewModel简介
-
ViewModel可以帮助Activity分担一部分工作,它是专门用于存放与界面相关的数据的。也就是说,只要是界面上能看得到的数据,它的相关变量都应该存放在ViewModel中,而不是Activity中,这样可以在一定程度上减少Activity中的逻辑。
-
另外,ViewModel还有一个非常重要的特性。我们都知道,当手机发生横竖屏旋转的时候,Activity会被重新创建,同时存放在Activity中的数据也会丢失。**而ViewModel的生命周期和Activity不同,它可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当Activity退出的时候才会跟着Activity一起销毁。**因此,将与界面相关的变量存放在ViewModel当中,这样即使旋转手机屏幕,界面上显示的数据也不会丢失。
-
使用ViewModel要先添加依赖
-
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
-
我们可以使用如下代码定义一个ViewModel,并加入一个counter变量用于计数:
class MainViewModel : ViewModel() {
var counter = 0
}
然后在Activity中使用如下代码即可获得ViewModel的实例:
- ViewModelProviders获取ViewModel而不是每次new出一个实例,
- 因为ViewModel有生命周期,不会每次都去创建一个实例,否则屏幕旋转就无法保存数据
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//ViewModelProviders获取ViewModel
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
viewMode.counter++//获取ViewModel内部的属性,并且操作
}
}
-
如果需要通过构造函数向ViewModel传递参数,但是ViewModel没有构造函数,所以需要借助ViewModelProvider.Factor
-
声明构造函数,并且赋值给counter变量
class MainViewModel(countReserved: Int) : ViewModel() {
var counter= countReserved
- 新建一个 MainViewModelFactory类,实现 ViewModelProvider.Factory接口
- MainViewModelFactory构造函数也接收一个 countReserved,传入给MainViewModel的构造函数
- 因为这里的create的方法和Activity的生命周期无关,所以可以创建MainViewModel
- 这样就实现了传递参数,可以用SP保存上次的数据,之后打开程序再从SP读取数据、传递数据给ViewModel
class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(countReserved) as T
}
}
Lifecycles
-
感知Activity的生命周期并不复杂,但问题在于,在一个Activity中去感知它的生命周期非常简单,而如果要在一个非Activity的类中去感知Activity的生命周期,应该怎么办呢?
- 旧的解决方法,比如通过在Activity中嵌入一个隐藏的Fragment来感知(Glide的做法),或者手写监听器的方式来感知
-
Lifecycles组件就是为了解决这个问题而出现的,它可以让任何一个类都能轻松感知到Activity的生命周期,同时又不需要在Activity中编写大量的逻辑处理。
-
MyObserver类实现LifecycleObserver接口
-
定义方法,使用注解表示生命周期
class MyObserver : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun activityStart() {
Log.d("MyObserver", "activityStart")
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun activityStop() {
Log.d("MyObserver", "activityStop")
}
}
在Activity中调用
- 因为Activity本身就是一个LifecycleOwner实例,所以不需要getLifecycleOwner了,get Lifecycle直接addObserver
lifecycle.addObserver(MyObserver())
- 如果传入Lifecycle对象,那么就可以通过lifecycle.currentState获取生命周期,它一共有用五种状态
class MyObserver (var lifecycle: Lifecycle): LifecycleObserver {
LiveData
-
LiveData是Jetpack提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。
-
LiveData特别适合与ViewModel结合在一起使用,虽然它也可以单独用在别的地方,但是绝大多数情况下,它都是使用在ViewModel当中的。
-
LiveData内部使用了Lifecycles组件来自我感知生命周期,从而在Activity销毁的时候释放,避免内存泄漏
-
Activity不可见状态的时候,如果LiveData数据发生变化,不会通知给观察者,也是基于Lifecycles组件
-
我们可以将ViewModel中的数据使用LiveData来包装,然后在Activity中去观察它,就可以主动将数据变化通知给Activity了。
-
MainViewModel的counter是MutableLiveData()类型,是一个可变的LiveData,泛型是Int
- LiveData有三种方法,分别是getValue,setValue和postValue
- get是获取
- set用于设置,只能在主线程调用
- post用于在非主线程设置
- LiveData有三种方法,分别是getValue,setValue和postValue
-
在init代码块为counter.value赋值,实现之前保存的数据通过构造函数传入,再恢复
-
plusOne方法先获取,如果空就为0,再+1
class MainViewModel(countReserved: Int): ViewModel() {
val counter = MutableLiveData<Int>()
init {
counter.value = countReserved
}
fun plusOne() {
val count = counter.value ?: 0
counter.value = count + 1
}
fun clear() {
counter.value = 0
}
}
Activity中调用为
- viewModel.counter.observe方法来观察数据变化:经过刚才的改造,counter已经是一个LiveData对象,它的observe方法可以观察数据变化
- observe方法第一个参数是LifecycleOwner对象,可以传入this的Activity;第二个参数是一个Observer接口,当counter包含的数据发生变化,就会毁掉这里,于是更新到界面
plusOneBtn.setOnClickListener {
viewModel.plusOne()
}
clearBtn.setOnClickListener {
viewModel.clear()
}
viewModel.counter.observe(this, Observer{ count ->
infoText.text = count.toString()
})
- 使用KTX的这个库,在2.2.0版本加入队observe方法的语法扩展,支持Java函数式API的写法
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha05"
可以写为
viewModel.counter.observe(this) { count ->
infoText.text = count.toString()
})
- Android推荐的做法是,只暴露不可变的LiveData给外部,这样在非ViewModel中就只能观察LiveData的数据变化,而不能给LiveData设置数据
- 把原来的counter改为_counter并且是private的,这样_counter就是外部不可见的
- 然后定义了行的counter变量是不可变的LiveData,get方法返回_counter变量
- 这样,外部调用counter变量时,实际上获得是 _counter实例,但是无法给counter设置数据,保证了ViewModel的数据封装性
val counter: LiveData<Int>
get() = _counter
private val _counter = MutableLiveData<Int>()
init {
_counter.value = countReserved
}
fun plusOne() {
val count = _counter.value ?: 0
_counter.value = count + 1
}
fun clear() {
_counter.value = 0
}
map和switchMap
- 我们有一个User变量,外界只要知道姓名,不关心年级,那么就不能暴露整个User给外部
- 用一个private的MutableLiveData()
- 暴露给外界的是userName: LiveData
- Transformations.map只获取name属性的字符串
private val userLiveData = MutableLiveData<User>()
val userName: LiveData<String> = Transformations.map(userLiveData) { user ->
"${user.firstName} ${user.lastName}"
}
- 上面都是LiveData在ViewModel中构造的,如果LiveData是从数据库或者其他地方获取的,要用到switchMap,否则每次获取的都是一个新的LiveData,而观察的确实老的LiveData,而不是一个可观察的LiveData
- 外部调用getUser函数来获取用户数据时,会赋值给 userIdLiveData.value,所以发生了变化,那么就调用了观察它的Transformations.switchMap(userIdLiveData),于是获取从 Repository就get到真正的user给user这个LiveData
private val userIdLiveData = MutableLiveData<String>()
val user: LiveData<User> = Transformations.switchMap(userIdLiveData) { userId ->
Repository.getUser(userId)
}
fun getUser(userId: String) {
userIdLiveData.value = userId
}
- 调用处,观察的是user这个可以被观察的LiveData
getUserBtn.setOnClickListener {
val userId = (0..10000).random().toString()
viewModel.getUser(userId)
}
viewModel.user.observe(this, Observer { user ->
infoText.text = user.firstName
})
-
上面的例子是有传入userId作为参数的,但是如果没有参数呢?
-
把旧的数据取出来,重新赋值给自己,也能触发一次数据变化,如果refresh方法一样
private val refreshLiveData = MutableLiveData<Any?>()
val refreshResult = Transformations.switchMap(refreshLiveData) {
Repository.getUser("")
}
fun refresh() {
refreshLiveData.value = refreshLiveData.value
}
Room
- ORM(Object Relational Mapping)也叫对象关系映射。
- 我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是ORM了。
先来看一下Room的整体结构。它主要由Entity、Dao和Database这3部分组成,每个部分都有明确的职责,详细说明如下。
- Entity 用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
- Dao Dao是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程的时候,逻辑层就不需要和底层数据库打交道了,直接和Dao层进行交互即可。
- Database 用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供Dao层的访问实例。
- 配置
- Room会根据注解动态生成代码,所以使用kapt插件
apply plugin: 'kotlin-kapt'//插件
dependencies {
....
implementation "androidx.room:room-runtime:2.1.0"
kapt "androidx.room:room-compiler:2.1.0"
}
首先定义一个Entity,也就是实体类。
- 这里我们在User的类名上使用@Entity注解,将它声明成了一个实体类,
- 然后在User类中添加了一个id字段,并使用@PrimaryKey注解将它设为了主键,再把autoGenerate参数指定成true,使得主键的值是自动生成的。
@Entity
data class User(var firstName: String, var lastName: String, var age: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
接下来定义Dao,代码如下所示:
- @Dao注解给接口,才能识别成一个Dao
- @Insert注解,表示会将参数中传入的User对象插入数据库,插入完成后返回主键id
- @update注解,表示会将参数的User更新到数据库
- @Delete注解,表示会将参数User从数据库中删除
- 上面不用编写SQL,但是如果从数据库查询,或者使用非实体类参数来增删改,就要SQL语句
- 比如loadAllUsers查询所有user
- 还有deleteUserByLastName删除特定user,也需要使用@Query注解
- 如果SQL语句有语法错误,编译的时候会直接报错
@Dao
interface UserDao {
@Insert
fun insertUser(user: User): Long
@Update
fun updateUser(newUser: User)
@Query("select * from User")
fun loadAllUsers(): List<User>
@Query("select * from User where age > :age")
fun loadUsersOlderThan(age: Int): List<User>
@Delete
fun deleteUser(user: User)
@Query("delete from User where lastName = :lastName")
fun deleteUserByLastName(lastName: String): Int
}
最后定义Database,定义好3个部分
-
数据库版本号
-
包含哪些实体
-
以及提供Dao层的访问实例
-
头部的@Database实现了前两个部分,多个实体类之间用逗号隔开
-
AppDatabase 必须继承RoomDatabase,兵器一定要声明成为抽象类,提供抽象方法的声明
-
在 companion object编写一个单例模式,提供一个获取实例的方法
- 如果不空就返回
- 如果空就创建,给instance赋值,返回。创建有三个参数
- 第一个参数一定要context.applicationContext,不能是普通的context,否则会内存泄漏
- 第二个参数是AppDatabase的Class类型
- 第三个是数据库名
@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
private var instance: AppDatabase? = null
@Synchronized
fun getDatabase(context: Context): AppDatabase {
instance?.let {
return it
}
return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").build().apply {
instance = this
}
}
}
}
在Activity中使用如下代码来进行数据库操作了:
- 先get到UserDao,之后就能操作
- 由于Room数据库是耗时操作,默认不允许在主线程执行,要开启子线程
val userDao = AppDatabase.getDatabase(this).userDao()
val user1 = User("Tom", "Brady", 40)
val user2 = User("Tom", "Hanks", 63)
addDataBtn.setOnClickListener {
thread {
user1.id = userDao.insertUser(user1)
user2.id = userDao.insertUser(user2)
}
}
updateDataBtn.setOnClickListener {
thread {
user1.age = 42
userDao.updateUser(user1)
}
}
deleteDataBtn.setOnClickListener {
thread {
userDao.deleteUserByLastName("Hanks")
}
}
queryDataBtn.setOnClickListener {
thread {
for (user in userDao.loadAllUsers()) {
Log.d("MainActivity", user.toString())
}
}
- 但是可以在构建AppDatabase实例的时候,加上一个allowMainThreadQueries()。加上这句就可以在主线程执行,但是建议只在测试环境使用
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.allowMainThreadQueries()//加上这句就可以在主线程执行,但是建议只在测试环境使用
.build().apply {
instance = this
Room数据库升级
- 暂时没学,以后再学
WorkManager
- WorkManager很适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用AlarmManager实现还是JobScheduler实现,从而降低了我们的使用成本。
- WorkManger可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然会得到执行, WorkManager还支持周期性任务、链式任务处理等功能,是一个非常强大的工具。
dependencies {
implementation "androidx.work:work-runtime:2.2.0"
}
WorkManager的基本用法其实非常简单,主要分为以下3步:
- 定义一个后台任务,并实现具体的任务逻辑。
- 配置该后台任务的运行条件和约束信息,并构建后台任务请求。
- 将该后台任务请求传入WorkManager的enqueue()方法中,系统会在合适的时间运行。
第一步要定义一个后台任务,这里创建一个SimpleWorker类,代码如下所示:
- 每一个后台任务都要继承Worker,重写doWork方法,
- doWork方法不会允许在主线程
- doWork方法要求返回一个Result对象,表示任务结果
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
Log.d("SimpleWorker", "do work in SimpleWorker")
return Result.success()
}
}
第二步,配置后台任务的运行条件和约束信息,代码如下所示:
- 只需要传入刚才创建的后台Work任务的Class
- OneTimeWorkRequest是构建单词允许的任务
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setInitialDelay(5, TimeUnit.MINUTES)
.build()
- PeriodicWorkRequest构建周期性运行的任务,为了降低设备性能消耗,间隔不能短于15分钟
val request2=PeriodicWorkRequest.Builder(SimpleWorker::class.java,15,TimeUnit.MINUTES).build()
最后一步,将构建出的后台任务请求传入WorkManager的enqueue()方法中,系统就会在合适的时间去运行了,代码如下所示:
WorkManager.getInstance(context).enqueue(request)
- 指定延迟某个时间再运行
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setInitialDelay(5,TimeUnit.MINUTES)//5分钟后运行
.build()
-
可以设置tag,之后通过tag来取消任务
- 即使没有tag,也可以用id来取消
- 但是用tag可以取消同一tag的一批任务
-
也可以一次性取消所以后台任务
-
可以设置retry来重新执行
-
可以使用链式调用任务,比如时间同步数据、压缩、上传,按顺序执行
-
WorkManger在国产手机上不稳定,不能依赖它实现核心功能
DSL
-
DSL的全称是领域特定语言(Domain
Specific Language),它是编程语言赋予开发者的一种特殊能力,通过它我们可以编写出一些看似脱离其原始语法结构的代码,从而构建出一种专有的语法结构。 -
第9章的infix函数构建出的特有语法结构就是DSL
-
介绍了一个gradle和HTML的例子,暂时没学
第14章 继续进阶,你还应该掌握的高级技巧
全局获取context的技巧
- 实现自定义的Application类
- 在 companion object 定义一个context
class MyApplication : Application() {
companion object {
@SuppressLint("StaticFieldLeak")//消除编译器的报错
lateinit var context: Context
}
override fun onCreate() {
super.onCreate()
context = applicationContext
}
}
调用
Toast.makeText(MyApplication.context, this, duration).show()
使用intent传递对象
Serializabled方式
- 实现Serializable接口就可以了
class Person : Serializable{
...
}
Parcelable方式
- 实现 Parcelable 接口,还要重写方法
//class Person : Parcelable {
// var name = ""
// var age = 0
//
// override fun writeToParcel(parcel: Parcel, flags: Int) {
// parcel.writeString(name) // 写出name
// parcel.writeInt(age) // 写出age
// }
//
// override fun describeContents(): Int {
// return 0
// }
//
// companion object CREATOR : Parcelable.Creator<Person> {
// override fun createFromParcel(parcel: Parcel): Person {
// val person = Person()
// person.name = parcel.readString() ?: "" // 读取name
// person.age = parcel.readInt() // 读取age
// return person
// }
//
// override fun newArray(size: Int): Array<Person?> {
// return arrayOfNulls(size)
// }
// }
//}
- 简便写法是使用注解,并且字段要移动到主构造函数中
@Parcelize
class Person(var name: String, var age: Int) : Parcelable
定制日志工具
- 我们可以修改level变量的等级,控制要打印的日志的级别
- 开发阶段指定为VERBOSE,上线指定为 ERROR就不会打印日志暴露
object LogUtil {
private const val VERBOSE = 1
private const val DEBUG = 2
private const val INFO = 3
private const val WARN = 4
private const val ERROR = 5
private var level = VERBOSE
fun v(tag: String, msg: String) {
if (level <= VERBOSE) {
Log.v(tag, msg)
}
}
fun d(tag: String, msg: String) {
if (level <= DEBUG) {
Log.d(tag, msg)
}
}
fun i(tag: String, msg: String) {
if (level <= INFO) {
Log.i(tag, msg)
}
}
fun w(tag: String, msg: String) {
if (level <= WARN) {
Log.w(tag, msg)
}
}
fun e(tag: String, msg: String) {
if (level <= ERROR) {
Log.e(tag, msg)
}
}
}
第15章 进入实战,开发一个天气预报App
搭建MVVM项目架构
架构主要分成三部分 :
- Model是数据模型层
- View是界面展示
- ViewModel可以理解成一个连接数据模型和界面展示的桥梁,从而实现业务逻辑和界面展示分离
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fTjveEpw-1590314475000)(E:\Notes\Kotlin\assets\《代码》笔记.assets\1590240327219.png)]
- logic包存放业务逻辑
- dao存放数据访问对象
- model存放对象模型
- network存放网络相关代码
- ui包存放界面相关
- place和weather两个主要页面
搜索全球城市数据功能
- ViewModel层不再持有Activity引用,会出现缺context情况,所以使用全局context
- 并且token也可以在这里填入,方便获取
class SunnyWeatherApplication : Application() {
companion object {
const val TOKEN = "" // 填入你申请到的令牌值
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
}
override fun onCreate() {
super.onCreate()
context = applicationContext
}
}
- 按照接口的json来编写数据模型,也就是数据类data
- 由于json一些字段的命名和Kotlin的命名规范不一致,就用@SerializedName注解来建立映射,比如address变量
import com.google.gson.annotations.SerializedName
class PlaceResponse(val status: String, val places: List<Place>)
class Place(val name: String, val location: Location, @SerializedName("formatted_address") val address: String)
class Location(val lng: String, val lat: String)
- 编写Retrofit接口
- 这里的token直接获取并且拼接,需要动态改变的只有query一个参数,query一般传城市
interface PlaceService {
@GET("v2/place?token=${SunnyWeatherApplication.TOKEN}&lang=zh_CN")
fun searchPlaces(@Query("query") query: String): Call<PlaceResponse>
}
- 创建一个单例的Retrofit构建器,这基本是固定写法
- fun create 方法是对外暴露的方法,因为retrofit变量是private的,实际调用了这个retrofit的create
- inline内联函数+泛型实化,使得调用处不需要写XX::class.java
object ServiceCreator {
private const val BASE_URL = "https://api.caiyunapp.com/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
inline fun <reified T> create(): T = create(T::class.java)
}
- 定义一个统一的网络数据源的访问入口,对所有网络请求的API进行封装
- 创建一个SunnyWeatherNetwork单例类
- private val placeService是动态代理对象
- searchPlaces挂起函数函数调用了placeService接口的searchPlaces方法发起网络请求,并且使用await函数
- await函数是11章的封装的函数,适合retrofit,请求的时候会把当前的协程阻塞住,直到响应请求数据返回
object SunnyWeatherNetwork {
private val placeService = ServiceCreator.create(PlaceService::class.java)
suspend fun searchPlaces(query: String) = placeService.searchPlaces(query).await()
private suspend fun <T> Call<T>.await(): T {
return suspendCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null) continuation.resume(body)
else continuation.resumeWithException(RuntimeException("response body is null"))
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
}
- 定义一个统一的仓库层的封装入口
- 仓库层定义的方法,为了把数据响应给上一层,会返回一个LiveData对象
- 这里是用ktx库的LiveData函数,可以自动构建和返回一个LiveData对象,然后再代码块中提供一个挂起的函数的上下文,这里就可以在里面调用挂起函数
- 请求后判断是否ok
- 最后用emit发射,类似于调用LiveData的setValue方法来通知数据变化
- LiveData指定了Dispatchers.IO在子线程执行网络请求
object Repository {
fun searchPlaces(query: String) = fire(Dispatchers.IO) {
val placeResponse = SunnyWeatherNetwork.searchPlaces(query)
if (placeResponse.status == "ok") {
val places = placeResponse.places
Result.success(places)
} else {
Result.failure(RuntimeException("response status is ${placeResponse.status}"))
}
emit(result)
}
}
- 定义ViewModel层
- ArrayList用于缓存数据
class PlaceViewModel : ViewModel() {
private val searchLiveData = MutableLiveData<String>()
val placeList = ArrayList<Place>()//用于缓存界面上显示的城市数据,因为原则上相关数据要保存,稍后旋转屏幕才不会丢失
val placeLiveData = Transformations.switchMap(searchLiveData) { query ->
Repository.searchPlaces(query)
}
fun searchPlaces(query: String) {
searchLiveData.value = query
}
}
UI层代码
- RecyclerView的适配器,通用的写法就不复制出来记录了
- Fragment详细说一下
- 首先用了by lazy懒加载获取PlaceViewModel
- 完全不用关心它合适初始化、是否为空等条件
- onActivityCreated方法给RecyclerView设置了适配器,viewModel.placeList作为数据源
- 给输入框设置了监听,如果输入内容不为空就发起一次请求
- 解决了请求发起,还要获取数据才行,交给LiveData
- 我们对 viewModel.placeLiveData进行观察,当有诗句的饿时候就添加到viewModel.placeList,并且刷新适配器
class PlaceFragment : Fragment() {
val viewModel by lazy { ViewModelProviders.of(this).get(PlaceViewModel::class.java) }
private lateinit var adapter: PlaceAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_place, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val layoutManager = LinearLayoutManager(activity)
recyclerView.layoutManager = layoutManager
adapter = PlaceAdapter(this, viewModel.placeList)
recyclerView.adapter = adapter
searchPlaceEdit.addTextChangedListener { editable ->
val content = editable.toString()
if (content.isNotEmpty()) {
viewModel.searchPlaces(content)
} else {
recyclerView.visibility = View.GONE
bgImageView.visibility = View.VISIBLE
viewModel.placeList.clear()
adapter.notifyDataSetChanged()
}
}
viewModel.placeLiveData.observe(this, Observer{ result ->
val places = result.getOrNull()
if (places != null) {
recyclerView.visibility = View.VISIBLE
bgImageView.visibility = View.GONE
viewModel.placeList.clear()
viewModel.placeList.addAll(places)
adapter.notifyDataSetChanged()
} else {
Toast.makeText(activity, "未能查询到任何地点", Toast.LENGTH_SHORT).show()
result.exceptionOrNull()?.printStackTrace()
}
})
}
}