Android备课笔记

谷歌

一,Android 入门介绍

1.1 了解 Android

1.1.1 Android 系统架构

四层架构,五块区域

image-20220712181801766

  1. Linux 内核层 -> 驱动
  2. 系统运行库层 -> C/C++ 做的特性 & Android 运行时库(核心库/Dalvik虚拟机-> Android5 变成了ART)
  3. 应用框架层 -> API
  4. 应用层

1.1.2 Android 应用开发特色

  1. 四大组件

    Android系统四大组件分别是活动(Activity)、服务(Service)、广播接收器(Broadcast Receiver)和内容提供器(Content Provider)

  2. 系统控件

  3. SQLite 数据库

    轻量级、运算速度极快的嵌入式关系型数据库,支持标准的 SQL 语法,还可以通过 Android 封装好的 API 进行操作,让存储和读取数据变得非常方便。

  4. 其他

    地理位置定位 LBS(Location based server)多媒体 传感器

1.2 搭建环境

1.2.1 开发工具

  • Android SDK
  • Android Studio

1.2.2 搭建开发环境

  1. 下载最新版本的 Androi Studio (其实i)

    [官网](Download Android Studio & App Tools - Android Developers (google.cn))

  2. 具体步骤百度一下

1.3,初识 Android Studio

IDE:Android Studio 2021.2.1

1.3.1 建立第一个项目

image-20220711111745247

image-20220711112033987

下面这张图有个注意的点:Name首字母记得最好大写,OK 了记得 Finish

image-20220711112416392

1.3.2 结构介绍

1.3.2.1 project 结构介绍
  1. 首先还原到本来目录

    image-20220713142105478

  2. 顶部工具部分介绍image-20220713142441928

  3. project 目录介绍

    image-20220713144123374

1.3.2.2 app目录下的结构

image-20220713145424043

1.3.2.3 res 目录下的结构

image-20220713175143211

怎么引用 res 中的资源

写快了,是string不是大写的String,大家自己改一下

image-20220713185133726

这里的 string 可以换成其他的,比如图片换成 drawable ,布局换成 layout

1.3.2.4 build.gradle 详解
外面那个

image-20220713193351905

里面那个
  1. plugin

    image-20220713193750507

  2. android

    image-20220713194021612

    image-20220713194240299

    image-20220713194656710

    image-20220713194913827

1.4 第一个 Android 程序解读

1.4.1 AndroidMainfest.xml 解读

image-20220713151718215

这里也可以验证上文说的怎么访问 res 中的资源是不是都是用的 @xxx/xxx

1.4.2 MainActivity 解读

image-20220713153247483

这里的类的继承结构如下

image-20220713153903207

1.4.3 layout 中的 main_activity 界面

image-20220713155301740

1.5 日志

1.5.1 介绍

Android 日志工具类 Log(android.util.Log)

1.5.1.1 Logcat

image-20220713225537462

1.5.1.2 日志级别

下面级别依次递增

  • Log.v() verboser
  • Log.d() debug
  • Log.i() info
  • Log.w() warn
  • Log.e() erro

演示:image-20220713225058498

1.5.1.3 Log 过滤器
  1. 级别image-20220713230137754

  2. 自己做一个过滤器

    image-20220713230718084

    image-20220713230957654

1.5.2 为什么用 Log 不用 println

println 除了方便打出来没有啥优点,去看看鱼皮有个 bug 分享,这个 println 的危害,里面可见一斑。

然而 Log 的级别控制,过滤器,关键字输入框 虽然不是完美但是已经很强大了。

二,Kotlin


2.0 Kotlin 认识篇

Kotlin 的历史:

  1. JetBrains 2011 Kotlin 出生 2016 1.0 正式成熟

  2. 开头 9 年都是用 Java 开发 Android 。Android 1.5 引入NDK支持 C/C++ 本地化开发

  3. 2017 Google I/O 宣布 Kotlin 开发 Android 地位和 Java 一样

  4. 2019 Google I/O 宣布 Kotlin First 强推用 Kotlin 开发Android

Kotlin 现状:

国外:Kotlin 很火 Google pay 应用商店排名前 1000 ,60%+用的 Kotlin 开发。 Android官网文档的代码已优先显示Kotlin版本,官方的视频教程以及Google的一些开源项目,也改用了 Kotlin 来实现。

国内: 新兴,保守,过于依赖 Java ,加上敏捷开发,跨端框架,Kotlin 在发展。但坑定是未来版本之王。

Kotlin 的优点:

  1. 和 Java 100% 兼容 直接调用 Java 代码,使用 Java 第三方库
  2. 简洁 相对Java 代码量50%-
  3. 高级 现代高级语法特性
  4. 安全 NPE 空指针

在哪写 Kotlin?

  1. 用 IDEA 前提是你安装了 这个

  2. 在线官方运行网站:https://try.kotlinlang.org (非科学上网访问会有点慢)

    image-20220731151336530

  3. Android Studio 刚好上节课安装了这个我们就用这个来写

为啥 Android 是 Google 开发的系统,Kotlin 确是 JetBrains 开发出来的?

编程语言分类 -> Java 语言机制 -> Kotlin语言机制

那么如果我开发了一门新的编程语言,然后自己做了个编译器,让它将这门新语言的代码编译成同样规格的class文件,Java 虚拟机能不能识别呢?没错,这其实就是Kotlin的工作原理了


2.1 变量

val:value 给了初始值不能改变 (通常情况下,先用这个)

var:variable 值可以改变

类型推导机制(变量延迟赋值就得显示声明)

全是对象数据类型

image-20220731152902768

为什么会有 val?

2.2 函数

function(Koltin) Method

什么是函数? 藏宝图

函数结构解释

自动代码补全 懒?手撕代码?不不不 引包

简化的语法糖(简洁优雅的甜头)演示一步步

1
2
3
4
5
6
7
//自己定义的函数(返回两个数中大的那个)
//1.标准写法
fun largerNumber(num1:Int,num2:Int):Int{
return max(num1,num2)
}
//2.Kotlin 语法糖 简洁写法
fun largerNumber_(num1: Int,num2: Int) = max(num1,num2)

注意:Kotlin函数参数是不可变的,也就是说固定val

2.3 逻辑控制

程序的执行语句主要分为3种:顺序语句、条件语句和循环语句。

2.3.1 选择

2.3.1.1 if
1
2
3
4
5
6
7
8
9
10
11
12
13
//3.用 if 实现(最传统的办法)
fun largerNumber03(num1: Int, num2: Int): Int {
var value = 0
if (num1 > num2) {
value = num1
} else {
value = num2
}
return value
}

//4. 用 if语句作为返回值,同时采用 Kotlin 语法糖极简代码
fun largerNumber04(num1: Int, num2: Int) = if (num1 > num2) num1 else num2
2.3.1.2 when
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
fun main() {
val name01 = "LMC"
val name02 = "Tomxxx"
val yourGrades01 = checkYourGrades01(name01)
val yourGrades02 = checkYourGrades02(name02)
println("$name01 `s score is $yourGrades01")//40
println("$name02 `s score is $yourGrades02")//100
}

//1. when 当返回值的查成绩
fun checkYourGrades01(name: String) =
when (name) {
"Jack" -> 45
"Tom" -> 89
"Marry" -> 67
"LML" -> 40
else -> 0
}
//2.判断类型
fun checkNumber(num: Number) {
when (num) {
is Int -> println("number is Int")
is Double -> println("number is Double")
else -> println("number not support")
}
}
//3. 不在 when 中传入参数
fun checkYourGrades02(name: String) = when{
name.startsWith("Tom") -> 100
name == "Jack" -> 23
name == "Marry" -> 93
name == "LML" -> 43
else -> 0
}


2.3.2 循环

2.3.2.1 for - in

可以用来遍历区间 数组和集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1,区间定义
val range = 1..10//range 表示 [1,10]
//2. for_in
//1) [] 双端闭区间
for (i in 1..10) {
println(i)
}
//2) {) 单端闭区间
for (i in 1 until 10){
println(i)
}
//3)跳跃着玩
for (i in 1..10 step 2){
println(i)
}
//4)降序 [10,1]
for (i in 10 downTo 1){
println(i)
}

2.4 面向对象编程

啥叫面向对象啊?

面向对象和面向过程的区别就是面向对象是有类的。这个类是一种概括性的,笼统性的描绘和共性封装,一般是名词。比如一个类是 人。然后类中有属性,函数。属性:比如名字身高,一般是名词。函数相当于人的行为,比如吃喝拉撒。然后用这个类,按照这个类创造的对象就是把类中这些东西填满的具体的人,比如人类型变成人对象的过程就是造小人。

2.4.1 类与对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main(){
val Jack = Person()
Jack.name = "Jack"
Jack.age = 20
Jack.eat()
}

class Person{
var name = ""
var age = 0
fun eat(){
println("$name is eating,she is $age years old")
}
}

这就是面向对象编程最基本的用法了,简单概括一下,就是要先将事物封装成具体的类,然后将事物所拥有的属性和能力分别定义成类中的字段和函数,接下来对类进行实例化,再根据具体的编程需求调用类中的字段和方法即可。

​ —— 《第一行代码》

2.4.2 继承与构造函数 ⭐

继承

为什么有继承?继承思想是什么?

人和学生老师的故事

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fun main(){
//1.演示类与对象
val Jack = Person( "Jack",20)
Jack.eat()
//2.继承和构造器
val Tom = Student("a123",100,"Tom",19)
val p1 = Student("a122",99)
val p2 = Student()
//1)一般来说,一个类都会自带一个无参主构造器,也可以在类名后面小括号中自己指定
//2)次构造函数 标识符号是 constructor。不过必须继承主构造函数
}

//继承的步骤
//1,父类 open
//2.子类 后面 :父类构造器
open class Person(name:String,age:Int){
var name = ""
var age = 0
fun eat(){
println("$name is eating,she is $age years old")
}
}

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

}
class Student01: Person{
constructor(name: String,age: Int):super(name, age)
}

构造函数

什么是构造函数? 类实例化的工具 出厂就有的一些设置 就命名

Kotlin 中 两种:主构造函数 次构造函数(几乎很少用,被替代性很高)

主构造函数
  1. 特点:没有函数体

  2. 如果没写就是每个类默认都有一个不带参数的主构造函数 也称无参构造函数

  3. 当然你也可以显示的给它指明参数

  4. 代码演示:

    1
    2
    3
    4
    class Student(val sno: String, val grade: Int) : Person() {
    }//小思考:为啥是 val 鸭?
    val student = Student("a123", 5)

  5. 如果想在主构造函数写点逻辑:

    init 结构体:

    1
    2
    3
    4
    5
    6
    class Student(val sno:String,val grade:Int):Person(){
    init{
    println("sno is $sno")
    println("grade is $grade")
    }
    }
  6. 主构造函数中声明了 val/var 会自动让这参数成为该类的字段 不加就是临时的

  7. 继承父类为啥要带个()呢?

这就涉及了Java继承特性中的一个规定,子类中的构造函数必须调用父类中的构造函数,这个规定在Kotlin中也要遵守。

次构造函数

(其实平常基本用不到这个次构造函数)

一个类只能有一个主构造函数,但是可以有多个次构造函数,次构造函数也可以实例化一个类。

唯一不同的就是次构造函数可以有函数体

Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。

1
2
3
4
5
6
7
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) {
}
}
最特殊的一种情况

类中只有次构造函数,没有主构造函数。

1
2
3
4
class Student : Person {
constructor(name: String, age: Int) : super(name, age) {
}
}

Student 类后边没有显示的调用主构造函数,但是有次构造函数。所以这个类是只有次构造函数,没有主构造函数的。

我们看子构造函数 ,因为没有主构造函数,所以她直接继承父构造函数(所以这个用的是 super 而不是 this)

因为 Student 类 是在次构造函数调用父类的构造函数,所以括号后面是不需要再次调用父类的构造函数,所以 Person 后没有括号


2022/07/31

2.4.3 接口与多态

接口:我们在接口中定义一系列抽象的方法,然后再具体的子类中去实现。举例子:不同生物对于吃这个动作

多态:面向接口编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//接口
interface Study{
fun readBook()
fun doHomework(){ println("你咋不写作业?") }
//可以有这个默认的函数体,如果子类没有重写就会用这个(JDK1.8)
}

//父类
open class Person(val name:String,val age:Int)
//继承的子类
class Student03(name:String,age:Int):Study,Person(name,age){
override fun readBook() {
println("$name is reading,he is $age years old")
}

override fun doHomework() {
println("$name is doing homework,he is $age years old")
}
}




//体现多态的函数
fun startStudy(s1:Study){
s1.readBook()
s1.doHomework()
}
//主函数运行
fun main() {
val student = Student03("Jack",19)
startStudy(student)
}

2.4.4 可见性修饰符

image-20220801114829487

public:默认,都可以访问

project:当前类和子类

private:当前类中可见

internal:当前模块可见

2.4.5 Kotlin 的两个特殊类

啥是数据类?啥是单例类?

为啥Kotlin要专门搞这两个类呢? Java 的三个无意义函数

数据类 date 单例类 object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun main() {
//测试多态函数继承
val Jack = Student03("Jack", 19)
startStudy(Jack)
//测试数据类 date
val dog01 = Dog("tom", "BLACK")
val dog02 = Dog("tom", "BLACK")
println(dog01)
println("dog01 == dog02?" + (dog01 == dog02))

Account.accountInfo()
}

//Kotlin 中的数据类 equal hashcode toString 全给你整好了。短短一行
data class Dog(val name: String, val color: String)

//单例类 永远只有一个实例对象
object Account {
fun accountInfo() {
println("这是一个实例类中的方法")
}
}
//这种写法虽然看上去像是静态方法的调用,但其实Kotlin在背后自动帮我们创建了一个Singleton类的实例,并且保证全局只会存在一个Singleton实例。

让我们看看如果用 Java 要怎么实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//数据类
public class Cellphone {
String brand;
double price;
public Cellphone(String brand, double price) {
this.brand = brand;
this.price = price;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Cellphone) {
Cellphone other = (Cellphone) obj;
return other.brand.equals(brand) && other.price == price;
}
return false;
}

@Override
public int hashCode() {
return brand.hashCode() + (int) price;
}
@Override
public String toString() {
return "Cellphone(brand=" + brand + ", price=" + price + ")";
}
}
//单例类
public class Singleton {
private static Singleton instance;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void singletonTest() {
System.out.println("singletonTest is called.");
}
}

2.5 Lambda 基础

Java 的话是 Java8 出现这个 Lambda,这个 Kotlin 从第一版就支持 Lambda ,而且的话,功能的话也更加强大。可以说 Lambda 是 Kotlin 的灵魂。

这一个章节只有 Lambda 的基础,Lambda 高级技巧比如:高阶函数,DSL 后面再单独开。

2.5.1 集合

集合的函数式 API 是入门 Lambda 编程的绝佳示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fun main() {

//list/set 类
//1.固定方法(不可变集合:只能读取,不能增删改)
val list01 = listOf<String>("张三","李四","王五")
for (i in list01){
println(i)
}
//2,可以增删改
val list02 = mutableListOf<Int>(1,2,3,4)
list02.add(5)
for (i in list02) {
println(i)
}

//map类
val map01 = mutableMapOf<String,Int>("Jack" to 1,"Tom" to 2,"Serrry" to 3)
map01["LML"] = 4
for (mutableEntry in map01) {
println("name is ${mutableEntry.key} num is ${mutableEntry.value}")
}
}

2.5.2 集合的函数式 API

Lambda的定义:一小段可以作为参数传递的代码

Lambda的语法结构:{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}

这是Lambda表达式最完整的语法结构定义。首先最外层是一对大括号,如果有参数传入到 Lambda 表达式中的话,我们还需要声明参数列表,参数列表的结尾使用一个->符号,表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码(虽然不建议编写太长的代码),并且最后一行代码会自动作为Lambda表达式的返回值。

(我们有好多种简化 Lambda 的办法)

常用函数式 API 的 Lambda 变化演示

maxBy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.swu.lmc.kotlinLearning.AndroidKotlin

/**
* @author 李敏灿
* @version 1.0
*/

fun main(){

//1.原始版本
val list = listOf<String>("Apple","Banana","orange","Pear","Grape","Watermelon")
val maxLengthFruit = list.maxBy { it.length }
println("max length fruit is $maxLengthFruit")
//其实 maxBy 就是参数为 Lambda 表达式的普通函数

//2. 证明maxBy 参数为 Lambda
val list02 = listOf<String>("Apple","Banana","orange","Pear","Grape","Watermelon")
val lambda = {fruit:String -> fruit.length}
val maxLengthFruit02 = list.maxBy(lambda)
println("max length fruit is $maxLengthFruit02")

maxBy函数的工作原理

解释图:image-20220801155611688

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    //3.简洁优化

//1)可以把 Lambda 直接写在函数中
val maxLengthFruit02 = list.maxBy({fruit:String -> fruit.length})
//2)Kotlin规定,当Lambda参数是函数的最后一个参数时,可以将Lambda表达式移到函数括号的外面
val maxLengthFruit02 = list.maxBy(){fruit:String -> fruit.length}
//3)如果Lambda参数是函数的唯一一个参数的话,还可以将函数的括号省略
val maxLengthFruit02 = list.maxBy{fruit:String -> fruit.length}
//4)Lambda表达式中的参数列表其实在大多数情况下不必声明参数类型
val maxLengthFruit02 = list.maxBy{fruit -> fruit.length}
//5)当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it 关键字来代替
val maxLengthFruit02 = list.maxBy{it.length}



}
map

集合中的map函数是最常用的一种函数式API,它用于将集合中的每个元素都映射成一个另外的值,映射的规则在Lambda表达式中指定,最终生成一个新的集合。比如,这里我们希望让所有的水果名都变成大写模式,就可以这样写:

1
2
3
4
5
6
7
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val newList = list.map { it.toUpperCase() }
for (fruit in newList) {
println(fruit)
}
}
filter

Lambda 中写过滤条件,会返回一个新的集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 //4.同时使用 filter&map函数
val filter = list.filter { it.length <= 5 }
.map{it.toUpperCase()}
for (s in filter) {
println(s)
}

//5.any&all
/**
any和all函数。其中any函数用于判断集合中是否至少存在一个元素满足指定条件,all函数用于判断集合中是否所有元素都满足指定条件。
*/
fun main() {
val list = listOf<String>("Jom", "Jack", "Jerry", "Jecenlee", "JX")
val hasX = list.any { it.contains("好") }
val hasHL = list.all { it.startsWith("J") }
println("This list has X ? $hasX \n all start with J? $hasHL")

}


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

2.5.3 Java 函数式 API 的使用

如果我们在Kotlin代码中调用了一个 Java 方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。

Java单抽象方法接口指的是接口中只有一个待实现方法,如果接口中有多个待实现方法,则无法使用函数式API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Lambda 在Java 改造上的体验
fun javaLambdaApIUse(){
//原版
Thread(object :Runnable{//Kotlin 中用 object代替Java中的匿名内部类
override fun run() {
println("Thread is running")
}
}).start()
//1.
Thread(Runnable{
println("Thread is running")
}).start()

//2。
Thread({
println("Thread is running")
}).start()
//Last 版本
Thread{
println("Thread is running")
}.start()
}

2.6 空指针检查

什么是 NPE

NPE(NullPointerException)Bug系统崩溃率最高

原因:因为空指针是一种不受编程语言检查的运行时异常,只能由
程序员主动通过逻辑判断来避免,但即使是最出色的程序员,也不可能将所有潜在的空指针异常全部考虑到。

Java 中长什么样

1
2
3
4
5
6
7
8
9
10
11
public void doStudy(Study study) {
study.readBooks();
study.doHomework();
}

public void doStudy(Study study) {
if (study != null) {
study.readBooks();
study.doHomework();
}
}

Kotlin 的解决办法

Kotlin默认所有的参数和变量都不可为空

将空指针异常的检查提前到了编译时期,如果我们的程序存在空指针异常的风险,那么在编译的时候会直接报错,修正之后才能成功运行,这样就可以保证程序在运行时期不会出现空指针异常了。

image-20220802150143819

1
2
3
4
5
6
fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
study.doHomework()
}
}

判空辅助工具

那么可为空的类型系统是什么样的呢?很简单,就是在类名的后面加上一个问号。

?.操作符。就是当对象不为空时正常调用相应的方法,当对象为空时则什么都不做。

1
2
3
4
fun doStudy(study: Study?) {
study?.readBooks()
study?.doHomework()
}

?:操作符。这个操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。

1
2
3
4
5
6
7
val c = if (a ! = null) {
a
} else {
b
}

val c = a ?: b

!! 和全局变量问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//返回文本的长度
fun getTextLength(text: String?): Int {
if (text != null) {
return text.length
}
return 0
}
//升级
fun textLength(text: String?) = text?.length ?: 0




//!! 的用法
fun printUpperCase(text: String?) {
println(text!!.toUpperCase())
//外面做了判空处理,里面是不知道的
}

fun main() {
val s1 = "abcde"
println(textLength(s1))//6
println(textLength(null))//0
if (s1 != null) {
printUpperCase(s1)
}
}

let函数

(Kotlin 标准函数 )既可以配合辅助判空问号,又可以解决全局变量问题

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
obj.let { obj2 ->
// 编写具体的业务逻辑
}
//例子
fun doStudy(study: Study?) {
study?.readBooks()
study?.doHomework()
}

fun doStudy(study: Study?) {
if (study != null) {www.blogss.cn
study.readBooks()
}
if (study != null) {
study.doHomework()
}
}

fun doStudy(study: Study?) {
study?.let { stu ->
stu.readBooks()
stu.doHomework()
}
}

fun startStudy(s1: Study01?) {
s1?.let {
it.readBook()
it.doHomework()
}
}

直接用 != null 判空 不行,是因为全局变量的值随时都有可能被其他线程所修改,即使做了判空处理,仍然无法保证if语句中的study变量没有空指针风险。从这一点上也能体现出let函数的优势。

2.7 字符串内嵌表达式 /函数的参数默认值

字符串内嵌表达式

1
2
3
4
5
fun main() {
//1.字符串内嵌表达式
val name = "Jack"
val age = 12
println("My name is ${name}. I`m $age years old")

函数的参数默认值

(介绍-》用函数传参随意-》代替次构造函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 //2.函数默认参数
val Jack = Boy(height = 100)
println(Jack)
}
data class Boy(val name:String = "", val age:Int = 0,val height:Int)
//这个函数默认值大多数情况下可以代替次构造函数 看下面
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) {
}
}
//用函数的参数默认值
class Student(val sno: String = "", val grade: Int = 0, name: String = "", age: Int = 0) :Person(name, age) {}

2.8 标准函数和静态方法

2.8.1 标准函数

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

2.8.1.1 with

啥时候用:当想对同一个对象有很多操作时

它可以在连续调用同一个对象的多个方法时让代码变得更加精简

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

1
2
3
4
val result = with(obj) {
// 这里是obj的上下文
"value" // with函数的返回值
}
1
2
3
4
5
6
7
8
9
10
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = with(StringBuilder()) {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
toString()
}
println(result)
2.8.1.2 run

跟上面差不多

不会直接调用,而是要在某个对象的基础上调用;其次run函数只接收一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文。

1
2
3
4
5
6
7
8
9
10
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().run {
append("Start eating fruits.\n")
for (fruit in list) {www.blogss.cn
append(fruit).append("\n")
}
append("Ate all fruits.")
toString()
}
println(result)
2.8.1.1 apply

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val result = obj.apply {
// 这里是obj的上下文
}
// result == obj


val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().apply {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())

2.8.2 定义静态方法

其实 Kotlin 中用单例类代替了 静态属性

不过,使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,而如
果我们只是希望让类中的某一个方法变成静态方法的调用方式该怎么办呢?这个时候就可以使
用刚刚在最佳实践环节用到的companion object了

1
2
3
4
5
6
7
8
9
10
class Util {
fun doAction1() {
println("do action1")
}
companion object {
fun doAction2() {
println("do action2")
}
}
}

不过,doAction2()方法其实也并不是静态方法,companion object这个关键字实际上会
在Util类的内部创建一个伴生类,而doAction2()方法就是定义在这个伴生类里面的实例方
法。只是Kotlin会保证Util类始终只会存在一个伴生类对象,因此调用Util.doAction2()方
法实际上就是调用了Util类中伴生对象的doAction2()方法。

然而如果你确确实实需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层

  1. @JvmStatic注解只能加在单例类或companion object中的方法上,如果你尝试加在一个普通方法上,会直接提示语法错误。
  2. 顶层方法指的是那些没有定义在任何类中的方法 :直接创建一个 Kotlin 的文件,然后在这个文件中直接写的都是顶层方法。这个在Kotlin 中随便在哪,都可以直接调用顶层方法。但是在 Java 中就要 文件名.方法名

2.9 延迟初始化和密封类

2.9.1 延迟初始化

延迟初始化使用的是lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量
进行初始化,这样就不用在一开始的时候将它赋值为null了

判断是否初始化:

1
2
3
if (!::adapter.isInitialized) {
adapter = MsgAdapter(msgList)
}

2.9.2 密封类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.lmc.kotlinadvanced.demo02

import java.lang.Error
import java.lang.Exception

//不用密封类
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仅仅是满足语法,要是 Result 还有别的继承了,但是方法中没有加上去,就会走else,抛出错误

//用密封类
sealed class Result01
class Success01(val msg:String):Result01()
class Failure01(val error: Exception ):Result01()

fun getResultMsg(result: Result01) = when(result){
is Success01 -> result.msg
is Failure01 -> result.error.message

}
  1. 这是因为当在when语句中传入一个密封类变量
    作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应
    的条件全部处理。
  2. 密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。

2.10 扩展函数和运算符重载

2.10.1 扩展函数

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

语法结构:

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

定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中了。

文件名虽然并没有固定的要求,但是我建议向哪个类中添加扩展函数,就定义一个同名的Kotlin文件,这样便于你以后查找。

不过通常来说,最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问
域。

2.10.2 运算符重载

Kotlin的运算符重载却允许我们让任意两个对象进行相加,或者是进行更多其他的运算操作。

运算符重载使用的是operator关键字,只要在指定函数的前面加上operator关键字,就可以实现运算符重载的功能了。

image-20220722170646244

1
2
3
4
5
6
7
8
class Money(val value: Int)

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

Kotlin允许我们对同一个运算符进行多重重载

1
2
3
4
5
6
7
8
9
10
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)
}
}

image-20220722171923295

2.11 高阶函数

2.11.1 定义高阶函数

高阶函数的定义。如果一个函数接收另一个函数作为参数,或者返回值的类型是
另一个函数,那么该函数就称为高阶函数。

函数类型。我们知道,编程语言中有整型、布尔型等字段类型,而Kotlin又增加了一个函数类型的概念。如果我们将这种函数类型添加到一个函数的参数声明或者返回值声明当中,那么这就是一个高阶函数了。

函数类型定义:(String, Int) -> Unit

因此,->左边的部分就是用来声明该函数接收什么参数的,多个参数之间使用逗号隔
开,如果不接收任何参数,写一对空括号就可以了。而->右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就使用 Unit

高阶函数声明:

1
2
3
fun example(func: (String, Int) -> Unit) {
func("hello", 123)
}

高阶函数的用途:高阶函数允许让函数类型的
参数来决定函数的执行逻辑。即使是同一个高阶函数,只要传入不同的函数类型参数,那么它
的执行逻辑和最终的返回结果就可能是完全不同的。

高阶函数实现的方式:

  1. 古板方式
  2. Lambda
  3. 匿名函数
  4. 成员引用

最简单的高阶函数学习示例 :

1
2
3
4
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}

使用高阶函数举例子:(最古板的一个)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fun plus(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minus(num1: Int, num2: Int): Int {
return num1 - num2
}

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}

fun main() {
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2, ::plus)
val result2 = num1AndNum2(num1, num2, ::minus)
println("result1 is $result1")
println("result2 is $result2")
}

用 Lambda 版本

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2) { n1, n2 ->
n1 + n2
}
val result2 = num1AndNum2(num1, num2) { n1, n2 ->
n1 - n2
}
println("result1 is $result1")
println("result2 is $result2")
}

这才是定义高阶函数完整的语法规则,在函数类型的前面加上ClassName. 就表示这个函数类型是定义在哪个类当中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
//改写了builder成为类似于apply的函数
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().build {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())
}

那么这里将函数类型定义到StringBuilder类当中有什么好处呢?==好处就是当我们调用build
函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文==,同时这也是apply函数
的实现方式。

2.11.2 内联函数的使用

高阶函数的实现原理

kotlin 下的高阶函数

1
2
3
4
5
6
7
8
9
10
11
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
fun main() {
val num1 = 100
val num2 = 80
val result = num1AndNum2(num1, num2) { n1, n2 ->
n1 + n2
}
}

编译成 Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static int num1AndNum2(int num1, int num2, Function operation) {
int result = (int) operation.invoke(num1, num2);
return result;
}
public static void main() {
int num1 = 100;
int num2 = 80;
int result = num1AndNum2(num1, num2, new Function() {
@Override
public Integer invoke(Integer n1, Integer n2) {
return n1 + n2;
}
});
}

这就是Kotlin高阶函数背后的实现原理。你会发现,原来我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式。这就表明,我们每调用一次Lambda表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。

为了解决这个问题,Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行时开销完全消除

内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字的声明即可,

image-20220722214829048

image-20220722214906593

2.11.3 noinline&crossinline

noinline

一个高阶函数中如果接收了两个或者更多函数类型的参数,这时我们给函数加上了inline关键字,那么Kotlin编译器会自动将所有引用的 Lambda表达式全部进行内联。

但是,如果我们只想内联其中的一个Lambda表达式该怎么办呢?

1
2
inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {
}

这里使用inline关键字声明了inlineTest()函数,原本block1和block2这两个函数类型参数所引用的Lambda表达式都会被内联。但是我们在block2参数的前面又加上了一个noinline关键字,那么现在就只会对block1参数所引用的Lambda表达式进行内联了。这就是noinline关键字的作用。

为什么Kotlin还要提供一个noinline关键字来排除内联功能呢?这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。

另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。

啥叫局部返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun printString(str: String, block: (String) -> Unit) {
println("printString begin")
block(str)
println("printString end")
}
fun main() {
println("main start")
val str = ""
printString(str) { s ->
println("lambda start")
if (s.isEmpty()) return@printString
println(s)
println("lambda end")
}
println("main end")
}

image-20220722215807013

内联函数 (非局部返回)

image-20220722220020840

crossinline

如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现
中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。

1
2
3
4
5
6
inline fun runRunnable(block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}

image-20220722220537230

这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。

而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。

也就是说,如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。

那么是不是在这种情况下就真的无法使用内联函数了呢?也不是,比如借助crossinline关键字就可以很好地解决这个问题:

1
2
3
4
5
6
inline fun runRunnable(crossinline block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}

那么这个crossinline关键字又是什么呢?前面我们已经分析过,之所以会提示图6.18所示的错误,就是因为==内联函数的Lambda表达式中允许使用return关键字==,和==高阶函数的匿名类实现中不允许使用return关键字==之间造成了冲突。而crossinline关键字就像一个契约,它用于保证在内联函数的 Lambda 表达式中一定不会使用return关键字,这样冲突就不存在了,问题也就巧妙地解决了。

声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda表达式中使用 return关键字进行函数返回了,但是仍然可以使用return@runRunnable的写法进行局部返回。总体来说,除了在 return 关键字的使用上有所区别之外,crossinline 保留了内联函数的其他所有特性。

2.11.4 高阶函数应用

2.11.4.1 简化SharedPreferences的用法

之前

1
2
3
4
5
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()

高阶函数升级

1
2
3
4
5
6
7
8
9
10
11
fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}

getSharedPreferences("data", Context.MODE_PRIVATE).open {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}

其实这个就是KTX扩展库中的edit函数实现

1
2
3
4
5
getSharedPreferences("data", Context.MODE_PRIVATE).edit {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}
2.11.4.2 简化ContentValues的用法

原来:

1
2
3
4
5
6
val values = ContentValues()
values.put("name", "Game of Thrones")
values.put("author", "George Martin")
values.put("pages", 720)
values.put("price", 20.85)
db.insert("Book", null, values)

Pair对象 Kotlin中一种A to B的数据结构

vararg:可变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}

Kotlin中的Smart Cast功能。在When/if中判断为某一类型如果符合就会自动转换为这种类型

优化一下下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()

1
2
3
val values = contentValuesOf("name" to "Game of Thrones", "author" to "George Martin",
"pages" to 720, "price" to 20.85)
db.insert("Book", null, values)

2.12 泛型基础和委托

2.12.1 泛型基础

在一般的编程模式下,我们需要给任何一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。

定义:

泛型类:

1
2
3
4
5
6
7
8
class MyClass<T> {
fun method(param: T): T {
return param
}
}

val myClass = MyClass<Int>()
val result = myClass.method(123)//return也是Int类型

泛型方法:

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
fun <T> method(param: T): T {
return param
}
}

//使用
val myClass = MyClass()
val result = myClass.method<Int>(123)
//因为自动类型推断
val myClass = MyClass()
val result = myClass.method(123)

泛型限制

不手动指定上界的时候,泛型的上界默认是Any? ,如果想不可为空那就指定为 Any

1
2
3
4
5
6
class MyClass {
//指定上限为Number
fun <T : Number> method(param: T): T {
return param
}
}

小小实践:

1
2
3
4
5
6
7
8
9
10
11
//只能作用在StringBuilder类上面
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}

//作用在所有类 类似 apply
fun <T> T.build(block: T.() -> Unit): T {
block()
return this
}

2.12.2 委托

委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。

2.12.2.1 类委托

将一个类的具体实现委托给另一个类去完成。

1
2
3
4
5
6
7
8
9
//巧妙利用 委托实现自己的Set类
class MySet<T>(val helperSet: HashSet<T>) : Set<T> {
override val size: Int
get() = helperSet.size
override fun contains(element: T) = helperSet.contains(element)
override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)
override fun isEmpty() = helperSet.isEmpty()
override fun iterator() = helperSet.iterator()
}

可以看到,MySet的构造函数中接收了一个HashSet参数,这就相当于一个辅助对象。然后在 Set 接口所有的方法实现中,我们都没有进行自己的实现,而是调用了辅助对象中相应的方法实现,这其实就是一种委托模式。

如果我们只是让大部分的方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,那么MySet就会成为一个全新的数据结构类,这就是委托模式的意义所在。

Kotlin中委托使用的关键字是by,我们只需要在接口声明的后面使用by关键字,再接上受委托的辅助对象,就可以免去之前所写的一大堆模板式的代码了,如下所示:

1
2
3
4
5
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
//里面写自己特有的东西
fun helloWorld() = println("Hello World")
override fun isEmpty() = false
}
2.12.2.2 委托属性

将一个属性(字段)的具体实现委托给另一个类去完成

语法结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass {
var p by Delegate()
}


class Delegate {
var propValue: Any? = null
//访问p就回去Delegate类的getValue方法
//myClass:声明该Delegate类的委托功能可以在什么类中使用
//prop:Kotlin中的一个属性操作类,可用于获取各种属性相关的值。
operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {//<*>这种泛型的写法表示你不知道或者不关心泛型的具体类型,只是为了通过语法编译而已
return propValue
}
//给p赋值就回去Delegate类的setValue方法,如果p是val下面这个方法可以没有
operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {//value 表示要赋给委托属性的值
propValue = value
}
}
2.12.2.3 懒加载分析

本质就是一个委派

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    private val uriMatcher by lazy {
val matcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(authority, "book", bookDir)
addURI(authority, "book/#", bookItem)
addURI(authority, "category", categoryDir)
addURI(authority, "category/#", categoryItem)
}
matcher
}
/*
*只有by才是Kotlin中的关键字,lazy在这里只是一个高阶函数而已。在lazy函数中会创建
并返回一个Delegate对象,当我们调用p属性的时候,其实调用的是Delegate对象的
getValue()方法,然后getValue()方法中又会调用lazy函数传入的Lambda表达式,这样
表达式中的代码就可以得到执行了,并且调用p属性后得到的值就是Lambda表达式中最后一行
代码的返回值。
*/

仿写一个自己的lazy函数

一个Later.kt文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
}

//懒加载只会使用一次,所以不存在再给p赋值的情况
}

fun <T> later(block: () -> T) = Later(block)

//测试可不可行的代码
val p by later {
Log.d("TAG", "run codes inside later block")
"test later"
}

2.13 infix 增强代码可读性

未被 infix 加工前

1
2
3
if ("Hello Kotlin".startsWith("Hello")) {
// 处理具体的逻辑
}

infix 开始加工

1
2
3
4
infix fun String.beginsWith(prefix: String) = startsWith(prefix)
if ("Hello Kotlin" beginsWith "Hello") {
// 处理具体的逻辑
}

infix 函数允许我们将函数调用时的小数点、括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编写程序,让代码看起来更加具有可读性。

infix 函数限制:
首先,infix 函数是不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中;
其次,infix 函数必须接收且只能接收一个参数

再看两个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list.contains("Banana")) {
// 处理具体的逻辑
}

infix fun <T> Collection<T>.has(element: T) = contains(element)

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list has "Banana") {
// 处理具体的逻辑
}


//to 源码
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

2.14 泛型高级特性

2.14.1 对泛型进行实化

Java中没有泛型实化这个概念,但是需要了解一个叫泛型擦除机制。

Java在JDK1.5之后才引入泛型,他的实现就是通过泛型擦除机制。泛型对于类
型的约束只在编译时期存在 ,JVM是识别不出来我们在代码中指定的泛型类型。

所以用不了 T::class.java 这种语法,因为 T的实际类型在运行的时候是已经抹去的。 不能拿到泛型的实际类型(泛型无法实化)

Kotlin 的解决方案:inline(内联函数代码替换)+ reified

1
2
3
4
5
6
7
8
9
inline fun <reified T> getGenericType() = T::class.java


fun main() {
val result1 = getGenericType<String>()
val result2 = getGenericType<Int>()
println("result1 is $result1")
println("result2 is $result2")

image-20221003143929760

泛型实化的应用:优化 Intent 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//之前
val intent = Intent(context, TestActivity::class.java)
intent.putExtra("param1", "data")
intent.putExtra("param2", 123)
context.startActivity(intent)

//现在 泛型实化+ 高阶函数
inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
val intent = Intent(context, T::class.java)
intent.block()
context.startActivity(intent)
}

startActivity<TestActivity>(context) {
putExtra("param1", "data")
putExtra("param2", 123)
}

Kotlin将能够识别出指定泛型的实际类型,并启动相应的Activity。

2.14.2 泛型的协变

在开始学习协变和逆变之前,我们还得先了解一个约定。一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为in位置,而它的返回值是输出数据的地方,因此可以称它为out位置

image-20220726155134887

1
2
3
open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)

Student 是 Persion 的子类,但是 List不能成为List的子类,否则将可能存在类型转换的安全隐患

安全隐患举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SimpleData<T> {
private var data: T? = null
fun set(t: T?) {
data = t
}
fun get(): T? {
return data
}
}
fun main() {
val student = Student("Tom", 19)
val data = SimpleData<Student>()
data.set(student)
handleSimpleData(data) // 实际上这行代码会报错,这里假设它能编译通过
val studentData = data.get()//我想拿到一个 Student 的里面确是一个 Teacher的实例
}
fun handleSimpleData(data: SimpleData<Person>) {
val teacher = Teacher("Jack", 35)
data.set(teacher)
}

即使Student是Person的子类,SimpleData并不是SimpleData的子类。

问题所在:在handleSimpleData()方法中向SimpleData里设置了一个Teacher的实例。如果SimpleData在泛型T上是只读的话,肯定就没有类型转换的安全隐患了

泛型协变定义:假如定义了一个MyClass的泛型类,其中 A 是 B 的子类型,同时MyClass又是MyClass的子类型,那么我们就可以称MyClass 在 T 这个泛型上是协变的。

但是如何才能让MyClass成为MyClass的子类型呢?刚才已经讲了,如果一个泛型类在其泛型类型的数据上是只读的话,那么它是没有类型转换安全隐患的。而要实现这一点,则需要让MyClass类中的所有方法都不能接收T类型的参数。换句话说,T只能出现在out位置上,而不能出现在in位置上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

class SimpleData<out T>(val data: T?) {
fun get(): T? {
return data
}
}


fun main() {
val student = Student("Tom", 19)
val data = SimpleData<Student>(student)
handleMyData(data)
val studentData = data.get()
}
fun handleMyData(data: SimpleData<Person>) {
val personData = data.get()
}

让 SimpleData 在泛型T上是协变的。

谈一下 List 简化源码:

1
2
3
4
5
6
7
public interface List<out E> : Collection<E> {
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
public operator fun get(index: Int): E
}

2.14.3 泛型的逆变

逆变与协变却完全相反。那么这里先引出定义吧,假如定义了一个
MyClass 的泛型类,其中A是B的子类型,同时 MyClass 又是MyClass
的子类型,那么我们就可以称 MyClass 在T这个泛型上是逆变的。

image-20220726161721389

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Transformer<T> {
fun transform(t: T): String
}

fun main() {
val trans = object : Transformer<Person> {
override fun transform(t: Person): String {
return "${t.name} ${t.age}"
}
}
handleTransformer(trans) // 这行代码会报错
}
fun handleTransformer(trans: Transformer<Student>) {
val student = Student("Tom", 19)
val result = trans.transform(student)
}

Transformer并不是Transformer的子类型 -> 解决就是 让 T 变为逆变

1
2
3
interface Transformer<in T> {
fun transform(t: T): String
}

为什么逆变的 T 不能出现在 out 位置上?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Transformer<in T> {
fun transform(name: String, age: Int): @UnsafeVariance T
}

fun main() {
val trans = object : Transformer<Person> {
override fun transform(name: String, age: Int): Person {
return Teacher(name, age)
}
}
handleTransformer(trans)
}
fun handleTransformer(trans: Transformer<Student>) {
val result = trans.transform("Tom", 19)//期望得到一个 Student 对象,实际却是一个 Teacher 对象
}

2.15 协程

轻量级的线程

线程要依靠操作系统的调度才能实现不同线程之间的切换,但是协程是在编程语言层面实现的,这大大提升了并发编程的运行效率。

1
2
3
4
5
6
7
8
9
10
fun foo() {
a()
b()
c()
}
fun bar() {
x()
y()
z()
}

//不开线程?开了线程?开协程?分别是什么情况?

协程允许我们在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关。

2.15.1 协程的基本用法

添加依赖:

(第二个依赖库是在Android项目中才会用到的 )

1
2
3
4
5
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
}

新建一个文件

开启协程

1
2
3
4
5
fun main() {
GlobalScope.launch {
println("codes run in coroutine scope")//现在这样是运行不起来的
}
}

Global.launch函数每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟着一起结束。

改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1 能打印出来那句话了
fun main() {
GlobalScope.launch {
println("codes run in coroutine scope")
}
Thread.sleep(1000)//给协程一些时间来执行里面的代码
}

//2 就打印第一句
fun main() {
GlobalScope.launch {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
Thread.sleep(1000)
}

加餐小知识:

delay()是非阻塞的挂起函数,它只会挂起当前协程,不会影响其他协程的运行;(这个函数只能在协程的作用域和其他挂起函数中调用)

但是 Thread.sleep 会阻塞当前的线程,这时候当前线程下的所有协程都会被阻塞。

让应用程序在协程中所有代码都运行完了之后才结束

1
2
3
4
5
6
7
fun main() {
runBlocking {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
}

runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题 。


创建多个协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
runBlocking {
launch {
println("launch1")
delay(1000)
println("launch1 finished")
}

launch {
println("launch2")
delay(1000)
println("launch2 finished")
}
}
}

注意这里的launch函数和我们刚才所使用的GlobalScope.launch函数不同。首先它必须在
协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。相比而言,
GlobalScope.launch函数创建的永远是顶层协程,这一点和线程比较像,因为线程也没有层级这一说,永远都是顶层的。

一小段测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
val start = System.currentTimeMillis()
runBlocking {
repeat(100000) {
launch {
println(".")
}
}
}
val end = System.currentTimeMillis()
println(end - start)
}
//10w次一秒就完了,但是用线程直接OOM

suspend关键字,使用它可以将任意函数声明成挂起函数(但无法给它提供协程作用域的),而挂起函数之间都是可以互相调用的

1
2
3
4
suspend fun printDot() {
println(".")
delay(1000)
}

怎末给任意挂起函数提供协程作用域 ?

1
2
3
4
5
6
suspend fun printDot() = coroutineScope {
launch {
println(".")
delay(1000)
}
}

coroutineScope函数和runBlocking函数还有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//验证上面案例
fun main() {
runBlocking {
coroutineScope {
launch {
for (i in 1..10) {
println(i)
delay(1000)
}
}
}
println("coroutineScope finished")
}
println("runBlocking finished")
}

虽然看上去coroutineScope函数和runBlocking函数的作用是有点类似的,但是
coroutineScope函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此是不会造成任何性能上的问题的。而runBlocking函数由于会挂起外部线程,如果你恰好又在主线程中当中调用它的话,那么就有可能会导致界面卡死的情况,所以不太推荐在实际项目中使用。

2.15.2 更多作用域构建

在上一小节中,我们学习了GlobalScope.launch、runBlocking、launch、
coroutineScope这几种作用域构建器, GlobalScope.launch和runBlocking函数是可以在任意地方调用的,coroutineScope函数可以在协程作用域或挂起函数中调用,而launch函数只能在协程作用域中调用。

runBlocking由于会阻塞线程,因此只建议在测试环境下使用。而GlobalScope.launch由于每次创建的都是顶层协程,一般也不太建议使用 。

为什么不太建议使用顶层协程?因为管理成本太高,网络请求和Activity关闭的例子。本来取消协程就比较麻烦,你还是顶层协程。


取消协程?不管是 GlobalScope .launch 函数还是 launch 函数,它们都会返回一个Job对象,只需要调用Job对象的 cancel() 方法就可以取消协程了

1
2
3
4
val job = GlobalScope.launch {
// 处理具体的逻辑
}
job.cancel()

实际项目更常见使用方法

1
2
3
4
5
6
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
// 处理具体的逻辑
}
job.cancel()

所有调用CoroutineScope的launch函数所创建的协程,都会被关联在Job对象的作用域下面。这样只需要调用一次cancel()方法,就可以将同一作用域内的所有协程全部取消

小总结:

CoroutineScope()函数 更适合实际项目/在main函数中的一些学习测试 还是用runBlocking函数最方便


launch函数只能用于执行一段逻辑,却不能获取执行的结果(因为返回值永远是一个Job对象) ,那我想要==创建一个协程并获取它的执行结果==怎末做?

async函数必须在协程作用域当中才能调用,它会创建一个新的子协程并返回一个Deferred对象,如果我们想要获取async函数代码块的执行结果,只需要调用Deferred对象的await()方法即可

1
2
3
4
5
6
7
8
fun main() {
runBlocking {
val result = async {
5 + 5
}.await()
println(result)
}
}

事实上,在调用了async函数之后,代码块中的代码就会立刻开始执行。当调用await()方法时,如果代码块中的代码还没执行完,那么await()方法会将当前协程阻塞住,直到可以获得async函数的执行结果。

小小验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//两个协程在串行
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val result1 = async {
delay(1000)
5 + 5
}.await()
val result2 = async {
delay(1000)
4 + 6
}.await()
println("result is ${result1 + result2}.")
val end = System.currentTimeMillis()
println("cost ${end - start} ms.")
}
}
//两个协程在并行
fun main() {
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()挂起函数

1
2
3
4
5
6
7
8
9
10
fun main() {
runBlocking {
val result = withContext(Dispatchers.Default) {
5 + 5
}
println(result)
}
}

//函数解释:调用withContext()函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为withContext()函数的返回值返回

协程他妈这么牛是不是就可以不用线程了?

Android中要求网络请求必须在子线程中进行,即使你开启了
协程去执行网络请求,假如它是主线程当中的协程,那么程序仍然会出错。这个时候我们就应
该通过线程参数给协程指定一个具体的运行线程。

线程参数

  1. Dispatchers.Default表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。

  2. Dispatchers.IO表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用 Dispatchers.IO

  3. Dispatchers.Main 则表示不会开启子线程,而是在 Android 主线程中执行代码,但是这个值只能在 Android 项目中使用,纯Kotlin 程序使用这种类型的线程参数会出现错误。

事实上,在我们刚才所学的协程作用域构建器中,除了coroutineScope函数之外,其他所有
的函数都是可以指定这样一个线程参数的,只不过withContext()函数是强制要求指定的,而
其他函数则是可选的。

2.15.3 使用协程简化回调的写法

上次用编程语言的回调机制实现了获取异步网络请求数据响应是通过匿名类做到的

1
2
3
4
5
6
7
8
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
// 得到服务器返回的具体内容
}
override fun onError(e: Exception) {
// 在这里对异常情况进行处理
}
})

有多少个地方要发起网络请求就要写多少次这样的匿名类实现,怎末更加方便?

suspendCoroutine 函数必须在协程作用域或挂起函数中才能调用,它接收一个 Lambda 表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行 Lambda 表达式中的代码。Lambda 表达式的参数列表上会传入一个 Continuation 参数,调用它的resume() 方法或 resumeWithException() 可以让协程恢复执行。

1
2
3
4
5
6
7
8
9
10
11
12
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)
}
})
}
}

可以看到,request()函数是一个挂起函数,并且接收一个address参数。在request()函
数的内部,我们调用了刚刚介绍的suspendCoroutine函数,这样当前协程就会被立刻挂起,而Lambda表达式中的代码则会在普通线程中执行。接着我们在Lambda表达式中调用
HttpUtil.sendHttpRequest()方法发起网络请求,并通过传统回调的方式监听请求结果。
如果请求成功就调用Continuation的resume()方法恢复被挂起的协程,并传入服务器响应
的数据,该值会成为suspendCoroutine函数的返回值。如果请求失败,就调用
Continuation 的 resumeWithException() 恢复被挂起的协程,并传入具体的异常原因。

用一下

1
2
3
4
5
6
7
8
9
//访问百度首页
suspend fun getBaiduResponse() {
try {
val response = request("https://www.baidu.com/")
// 对服务器响应的数据进行处理
} catch (e: Exception) {
// 对异常情况进行处理
}
}

由于 getBaiduResponse()是一个挂起函数,因此当它调用了request()函数时,当前的协程就会被立刻挂起,然后一直等待网络请求成功或失败后,当前协程才能恢复运行。这样即使不使用回调的写法,我们也能够获得异步网络请求的响应数据,而如果请求失败,则会直接进入catch语句当中。

事实上,suspendCoroutine函数几乎可以用于简化任何回调的写法

比如之前的 Retrofit 发起网络请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//之前
val appService = ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object : Callback<List<App>> {
override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
// 得到服务器返回的数据
}
override fun onFailure(call: Call<List<App>>, t: Throwable) {
// 在这里对异常情况进行处理
}
})

//协程简化
suspend fun <T> Call<T>.await(): T {
return suspendCoroutine { continuation ->
enqueue(object : Callback<T> {//因为拥有Call对象的上下文,座椅这里可以直接发起网络请求
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()
// 对服务器响应的数据进行处理
} catch (e: Exception) {
// 对异常情况进行处理
}
}

每次发起网络请求都要进行一次try catch处理也比较麻烦 ?

选择不处理。在不处理
如果发生了异常就会一层层向上抛出,一直到被某一层的函数处理了为止。因此,
我们也可以在某个统一的入口函数中只进行一次try catch,从而让代码变得更加精简。

2.16 编写好看的工具方法

2.16.1 求 N 个数的最大最小值

利用泛型

1
2
3
4
5
6
7
8
9
10
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
}

2.16.2 简化 Toast

1
2
3
4
5
6
7
8
9
10
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()
}


"This is Toast".showToast(context, Toast.LENGTH_LONG)

2.16.3 简化 Snackbar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()
}
fun View.showSnackbar(resId: Int, actionResId: Int? = null,
duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? = null) {
val snackbar = Snackbar.make(this, resId, duration)
if (actionResId != null && block != null) {
snackbar.setAction(actionResId) {
block()
}
}
snackbar.show()
}


//现在具体用

2.17 DSL 构建专有的语法结构

DSL的全称是领域特定语言(Domain Specific Language)

实现办法:

  1. infix也算
  2. 更多的是高级语言

2.17.1 模仿gradle

新建一个DSL.kt文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Dependency {
val libraries = ArrayList<String>()
fun implementation(lib: String) {
libraries.add(lib)
}
}
fun dependencies(block: Dependency.() -> Unit): List<String> {
val dependency = Dependency()
dependency.block()
return dependency.libraries
}


//然后我们就可以这样用啦
fun main() {
val libraries = dependencies {
implementation("com.squareup.retrofit2:retrofit:2.6.1")
implementation("com.squareup.retrofit2:converter-gson:2.6.1")
}
for (lib in libraries) {
println(lib)
}
}

2.17.2 动态生成HTML表格代码

DSL.kt文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Td {
var content = ""
fun html() = "\n\t\t<td>$content</td>"
}
class Tr {
private val children = ArrayList<Td>()
fun td(block: Td.() -> String) {
val td = Td()
td.content = td.block()
children.add(td)
}
fun html(): String {
val builder = StringBuilder()
builder.append("\n\t<tr>")
for (childTag in children) {
builder.append(childTag.html())
}
builder.append("\n\t</tr>")
return builder.toString()
}
}
class Table {
private val children = ArrayList<Tr>()
fun tr(block: Tr.() -> Unit) {
val tr = Tr()
tr.block()
children.add(tr)
}
fun html(): String {
val builder = StringBuilder()
builder.append("<table>")
for (childTag in children) {
builder.append(childTag.html())www.blogss.cn
}
builder.append("\n</table>")
return builder.toString()
}
}
fun table(block: Table.() -> Unit): String {
val table = Table()
table.block()
return table.html()
}

用Kotlin的DSL来动态生成一个表格所对应的HTML代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val html = table {
tr {
td { "Apple" }
td { "Grape" }
td { "Orange" }
}
tr {
td { "Pear" }
td { "Banana" }
td { "Watermelon" }
}
}
println(html)
}

image-20221031210057349

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() {
val html = table {
repeat(2) {
tr {
val fruits = listOf("Apple", "Grape", "Orange")
for (fruit in fruits) {
td { fruit }
}
}
}
}
println(html)
}

image-20221031210159736

2.18 Java 和 Kotlin 代码之间的转换

就是有这么一个问题:现在当然绝大多数App都是在Kotlin开发,但是其实有些老的App他是用Java开发的,所以说我们遇到了这种老的App怎么让它里面的Java转换成Kotlin语言。

其实AS为我们想到了这一点。

  1. 一段Java代码转Kotlin:

    直接把Java代码粘贴到一个kt文件,他会提示转化成Kotlin,你点下好的就行了。只不过这种是比较僵硬的,不会自动应用Kotlin各种高级特性。

  2. 一个Java文件转Kotlin:

    第一种方法:image-20221102100317005

    第二种方法:image-20221102100356121

  3. Kotlin文件看Java形式源码:

    image-20221102100529920

    image-20221102100645052

三,Activity

3.1 Activity 是什么

Activity是最容易吸引用户的地方,它是一种可以包含用户界面的组件,主要用于和用户进行交互。

3.2 怎么使用/创建 Activity

3.2.1 手动创建 Activity

image-20220715104519737

image-20220715104549669

image-20220715104632406

image-20220715104730586

image-20220715105026070

image-20220715105244308

3.2.2 布局创建和加载

Android程序的设计讲究逻辑和视图分离,最好每一个Activity都能对应一个布
局。布局是用来显示界面内容的

image-20220715105335188

image-20220715105413028

image-20220715105717805

image-20220715105823166

image-20220715110217823

如果你需要在XML中引用一个id,就使用@id/id_name这种语法,而如果你需要在XML中定义一个id,则要使用@+id/id_name这种语法

这里使用 match_parent 表示让当前元素和父元素一样宽。android:layout_height 指定了当前元素的高度,这里使用 wrap_content 表示当前元素的高度只要能刚好包含里面的内容就行

layout 中用 xml 开始写

image-20220715111747048

image-20220715112017286

3.2.3 在AndroidManifest文件中注册和配置主启动器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.lmc.myfirstemptyactivitytest">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyFirstEmptyActivityTest"
tools:targetApi="31">
<activity
android:name=".FirstActivity"
android:label="This is FirstActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

image-20220715120003845

另外需要注意,如果你的应用程序中没有声明任何一个Activity作为主 Activity,这个程序仍然是可以正常安装的,只是你无法在启动器中看到或者打开这个程序。这种程序一般是作为第三方服务供其他应用在内部进行调用的。

上面就是手动创建了一个 Acticity


3.2.4 Activity中使用Toast

Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间

带 show() 啊!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout)

//findViewById() 传入一个 Id 返回一个继承 View 的对象 获取布局文件中控件的实例
val button = findViewById<Button>(R.id.firstButton)
//setOnClickListener() 设置点击逻辑
//Toast.makeText 就是写小提示的,这里有三个参数 ,
// context类或者子类,text(显示内容),持续时间:Toast.LENGTH_SHORT/Toast.LENGTH_LONG
button.setOnClickListener{
Toast.makeText(this,"在干嘛呢?",Toast.LENGTH_SHORT).show()
}
}
}

注意

  1. 以前有在后面 as 的,有在前面显式注明变量类型的,现在是直接泛型指定
  2. 那个插件是 ‘kotlin-android-extensions’ 也被弃用了,第一行代码这个地方有所出入。(AS4.1之后不默认引用,而且弃用了现在)
  3. 关于获取xml中控件的手段有好几种,这里贴上一篇博客:https://lmc.pub/2022/07/30/2022-07-30-《吐血推荐!全网最硬核Android视图绑定》/

image-20220715164525725

点下按钮,效果图:

Screenshot_20220715_163816_com.lmc.myfirstemptyac

备注:findViewById() 少用,用这个插件生成的对应控件 id

image-20220715165441951

1
2
3
firstButton.setOnClickListener{
Toast.makeText(this,"在干嘛呢?",Toast.LENGTH_SHORT).show()
}

3.2.5 Activity使用Menu

image-20220715170943455

image-20220715171049599

image-20220715171130667

image-20220715171229920

main.xml中是下面这个:

1
2
3
4
5
6
7
8
9
<?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>

重写onCreateOptionsMenu()方法

Ctrl+O找到这个或者直接打名字会有提示

下图眼花选错了,是onCreateOptionsMenu

image-20220715171825270

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//设计菜单
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main,menu)
return true//菜单显不显示
/**
* menuInflater就使用了这种语法糖,
它实际上是调用了父类的getMenuInflater()方法。getMenuInflater()方法能够得到一个MenuInflater对象,再调用它的inflate()方法,就可以给当前Activity创建菜单了。
inflate()方法接收两个参数:第一个参数用于指定我们通过哪一个资源文件来创建菜单,这里当然是传入R.menu.main;第二个参数用于指定我们的菜单项将添加到哪一个Menu对象当中,这里直接使用onCreateOptionsMenu()方法中传入的menu参数。最后给这个方法返回
true,表示允许创建的菜单显示出来,如果返回了false,创建的菜单将无法显示。
*/
}

//让菜单有用
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
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
}

搞出来什么样子:

Screenshot_20220715_212211_com.lmc.myfirstemptyac

3.2.6 销毁 Activity

代码形式就是一个 finish() 函数没了

3.3 Intent 让 Activity 穿梭

Intent一般可用于启动Activity、启动Service以及发送广播等场景

3.3.1 显式 Intent

新建 Activity

  1. 建立Intent

  2. 启动Activity

image-20220715213514609

image-20220715213646954

image-20220715214043296

1
val intent = Intent(this, SecondActivity::class.java)//显式 Intent

Intent 有多个构造函数的重载,其中一个是 Intent(Context packageContext, Class<?
> cls)。这个构造函数接收两个参数:第一个参数 Context 要求提供一个启动 Activity 的上下文;第二个参数Class用于指定想要启动的目标Activity,通过这个构造函数就可以构建出 Intent 的“意图”。那么接下来我们应该怎么使用这个Intent呢?Activity 类中提供了一个
startActivity() 方法,专门用于启动 Activity,它接收一个 Intent 参数,这里我们将构建好
的 Intent 传入 startActivity() 方法就可以启动目标 Activity 了。

3.3.2 隐式 Intent

相比于显式 Intent,隐式 Intent 则含蓄了许多,它并不明确指出想要启动哪一个Activity,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的Activity去启动

1
2
val intent = Intent("com.lmc.myfirstemptyactivitytest.ACTION_START")
intent.addCategory("com.lmc.myfirstemptyactivitytest.MY_CATEGORY")//隐式 Intent
1
2
3
4
5
6
7
8
9
10
<activity
android:name=".SecondActivity"
android:exported="true">
<intent-filter>
<action android:name="com.lmc.myfirstemptyactivitytest.ACTION_START" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.lmc.myfirstemptyactivitytest.MY_CATEGORY" />
</intent-filter>
</activity>

每个Intent中只能指定一个action,但能指定多个category

上面是在自己 app 中 跳跃,其实也可以在不同的 app 中 Intent

在第二个页面中写

1
2
3
4
5
6
//点这个按钮就会打开浏览器然后进入那个网址
button02.setOnClickListener{
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://lmc.pub")
startActivity(intent)
}

与此对应,我们还可以在标签中再配置一个标签,用于更精确地指
定当前Activity能够响应的数据。标签中主要可以配置以下内容。
android:scheme。用于指定数据的协议部分,如上例中的https部分。
android:host。用于指定数据的主机名部分,如上例中的www.baidu.com部分。
android:port。用于指定数据的端口部分,一般紧随在主机名之后。
android:path。用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内
容。
android:mimeType。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
只有当标签中指定的内容和Intent中携带的Data完全一致时,当前Activity才能够响应
该Intent。不过,在标签中一般不会指定过多的内容。例如在上面的浏览器示例中,其
实只需要指定android:scheme为https,就可以响应所有https协议的Intent了。

1
2
3
4
5
6
7
8
9
<activity
android:name=".ThirdActivity"
android:exported="true"><!--表示我这个 Activity 能不能被外面的隐式调用 -->
<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>

https 协议响应不了,因为一般系统会绑定打开 https 协议的浏览器

除了https协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。

1
2
3
4
5
6
7
8
9
10
11
class ThirdActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_third)
button03.setOnClickListener {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:15243599513")
startActivity(intent)
}
}
}

image-20220716151103285

3.3.3 传递数据

3.3.3.1 向下传递数据

数据发送 Activity:

image-20220716152807000

1
2
3
4
5
6
7
val data = "Hello SecondActivity"
val intent = Intent(this, SecondActivity::class.java)//显式 Intent
intent.putExtra("extra_data",data)
//下面是隐式 Intent
//val intent = Intent("com.lmc.myfirstemptyactivitytest.ACTION_START")
// intent.addCategory("com.lmc.myfirstemptyactivitytest.MY_CATEGORY")//隐式 Intent
startActivity(intent)

接受方:

image-20220716152835003

1
2
3
//接受 Activity1 发我的信息并显示
val stringExtra = intent.getStringExtra("extra_data")
Log.d("SecondActivity","extra_data is $stringExtra")

image-20220716153028170

3.3.3.2 向上传递数据

其实Activity类中还有一个用于启动Activity的startActivityForResult()方法,但它期望在Activity销毁的时候能够返回一个结果给上
一个Activity。

注意:!!!《第一行代码》的startActivityForResult已经被废弃,现在是用下面演示的 ActivityResult API了

  1. FirstActivity 中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    class MainActivity : AppCompatActivity() {

    private lateinit var binding: FirstLayoutBinding

    val activityResultLauncher =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()
    ) { result ->
    if ( result.resultCode == RESULT_OK) {
    val returnData = result.data?.getStringExtra("data_return")
    Log.d( "FirstActivity", "returned data is $returnData" )
    }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.first_layout)

    binding = FirstLayoutBinding.inflate(layoutInflater)
    setContentView(binding.root)

    binding.button03.setOnClickListener {
    val data = "Hello SecondActivity"
    val intent = Intent("com.lmc.myapplication01.ACTION_START")
    intent.addCategory("com.lmc.application01.MY_CATEGORY")
    intent.putExtra("extra_data", data)

    activityResultLauncher.launch(intent)
    }

    }

    }


  2. SecondActivity 中 onCreate 中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //点这个按钮给 Activity1 传递数据
    button2_2.setOnClickListener {
    val intent = Intent()
    intent.putExtra("data_return", "Hello FirstActivity")
    setResult(RESULT_OK, intent)
    finish()
    /**
    * 可以看到,我们还是构建了一个Intent,只不过这个Intent仅仅用于传递数据而已,它没有指定任何的“意图”。紧接着把要传递的数据存放在Intent中,然后调用了setResult()方法。这个方法非常重要,专门用于向上一个Activity返回数据。setResult()方法接收两个参数:第一个参数用于向上一个Activity返回处理结果,一般只使用RESULT_OK或RESULT_CANCELED这两个值;第二个参数则把带有数据的Intent传递回去。最后调用了finish()方法来销毁当前Activity
    */
    }
    //设置通过 Back 键也会返回数据
    override fun onBackPressed() {
    val intent = Intent()
    intent.putExtra("data_return", "Hello FirstActivity")
    setResult(RESULT_OK, intent)
    finish()
    }

3.4 Activity的生命周期

3.4.1 返回栈

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

image-20220716163841692

3.4.2 Activity状态

  1. 运行状态:在栈顶,全部可见,最不会回收
  2. 暂停状态:不在栈顶,但是看得见,内存极低才会回收
  3. 停止状态:不在栈顶,页面已经看不见,内存不够就会回收
  4. 销毁状态:已经出栈了,最爱回收

3.4.3 Activity的生存期

Activity类中定义了7个回调方法,覆盖了Activity生命周期的每一个环节

  • onCreate()。这个方法你已经看到过很多次了,我们在每个Activity中都重写了这个方
    法,它会在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 生存期

  • 完整生存期。Activity在onCreate()方法和onDestroy()方法之间所经历的就是完整生存期。一般情况下,一个Activity会在onCreate()方法中完成各种初始化操作,而在
    onDestroy()方法中完成释放内存的操作。
  • 可见生存期。Activity在onStart()方法和onStop()方法之间所经历的就是可见生存期。在可见生存期内,Activity对于用户总是可见的,即便有可能无法和用户进行交互。我们可以通过这两个方法合理地管理那些对用户可见的资源。比如在onStart()方法中对资源进行加载,而在onStop()方法中对资源进行释放,从而保证处于停止状态的Activity不会占用过多内存。
  • 前台生存期。Activity在onResume()方法和onPause()方法之间所经历的就是前台生存
    期。在前台生存期内,Activity总是处于运行状态,此时的Activity是可以和用户进行交互
    的,我们平时看到和接触最多的就是这个状态下的Activity

image-20220716164955614

3.4.4 体验Activity的生命周期

怎么设置一个 Activity 为 Dialog 类型的?

1
2
3
<activity android:name=".DialogActivity"
android:theme="@style/Theme.AppCompat.Dialog">
</activity>

image-20220716172945559

3.4.5 Activity被回收了怎么办

用户在Activity A的基础上启动了Activity B,Activity A就进入了停止状态,这个时候由于系统内存不足,将Activity A回收掉了,然后用户按下Back键返回 Activity A,会出现什么情况呢?其实还是会正常显示Activity A的,只不过这时并不会执行onRestart()方法,而是会执行Activity A的onCreate()方法,因为Activity A在这种情况下会被重新创建一次。

那数据丢失怎么解决?

onSaveInstanceState()回调方法 ,这个方法可以保证在Activity被回收之前一定会被调用

onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的内容。

MainActivity中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val tempData = "Something you just typed"
outState.putString("data_key", tempData)
}


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
setContentView(R.layout.activity_main)
if (savedInstanceState != null) {
val tempData = savedInstanceState.getString("data_key")
Log.d(tag, "tempData is $tempData")
}
...
}

另外,当手机的屏幕发生旋转的时候,Activity也会经历一个重新创建的过程

3.5 Activity的启动模式

启动模式一共有5种,分别是standard、singleTop、singleTask和singleInstance,singleInstancePerTask

standard:可以创建无数个Activity实例。

singleTop:如果当前Task的top如果已经是正在启动的这个Activity,那就不要重复启动。

singleTask:这个Activity只能有一个,且作为它所在的Task的root Activity(对于taskAffinity不同的情况)。

singleInstance:这个Activity只能有一个,且它所在的Task也只能有它这一个Activity。

singleInstancePerTask:是singleTask的扩展,这个Activity可以有多个实例,但是每个都是所在的Task的root Activity。

3.5.1 standard

standard是Activity默认的启动模式,在不进行显式指定的情况下,所有Activity都会自动使用这种启动模式。在standard模式下,每当启动一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的Activity,系统不会在乎这个Activity是否已经在返回栈中存在,每次启动都会创建一个该Activity的新实例

image-20220716201723585

3.5.2 singleTop

适用最多的

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

android:launchMode=“singleTop”

1
2
3
4
5
6
7
8
9
<activity
android:name=".FirstActivity"
android:launchMode="singleTop"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

FirstActivity中onCreate()方法的代码 :

1
2
3
4
5
6
7
8
9
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("FirstActivity", this.toString())
setContentView(R.layout.first_layout)
button1.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
}

image-20220716202704338

3.5.3 singleTask

使用singleTop模式可以很好地解决重复创建栈顶Activity的问题,但是正如你在上一节所看到的,如果该Activity并没有处于栈顶的位置,还是可能会创建多个Activity实例的

**让某个Activity在整个应用程序的上下文中只存在一个实例 **

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

image-20220716203332324

3.5.4 singleInstance

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

该 activity 始终是其任务中的唯一 activity。

image-20220716205218343

3.5.5 singleInstancePerTask

Android 12 新加的,《第一行代码》没加上

该Activity只能作为Task的root Activity运行,即创建该Task的第一个Activity,因此在一个Task中只能有一个该Activity的实例。与singleTask启动模式相比,如果设置了FLAG_ACTIVITY_MULTIPLE_TASK或FLAG_ACTIVITY_NEW_DOCUMENT,这个activity可以在不同的Task中多个实例中启动。

  1. 以MainActivity为起点,启动SingleInstancePerTaskActivity:image-20220808211506057

  2. 当SingleInstancePerTaskActivity位于当前Task top时启动SingleInstancePerTaskActivity:

    发现SingleInstancePerTaskActivity不会重复启动,而是现有的实例收到Activity.onNewIntent回调:

  3. 当SingleInstancePerTaskActivity没有位于当前Task top时启动SingleInstancePerTaskActivity:

    image-20220808211705951

  4. 当SingleInstancePerTaskActivity存在的Task没有处于前台时去启动SingleInstancePerTaskActivity:

    image-20220808211732693

  5. 结合Intent.FLAG_ACTIVITY_MULTIPLE_TASK和Intent.FLAG_ACTIVITY_NEW_DOCUMENT

    1
    intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);

    该launchMode和Intent.FLAG_ACTIVITY_MULTIPLE_TASK或Intent.FLAG_ACTIVITY_NEW_DOCUMENT结合使用可以将启动的Activity在多个Task中多次实例化

    image-20220808212039656

这篇文章讲的非常到‘胃’:https://juejin.cn/post/7071188263968440356

3.6 Activity的最佳实践

3.6.1 知晓当前是在哪一个Activity

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

  1. 建个 baseActivity

    1
    2
    3
    4
    5
    6
    open class BaseActivity :AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
    super.onCreate(savedInstanceState, persistentState)
    Log.d("BaseActivity",javaClass.simpleName)
    }
    }

    Bug记录:

    按上面这么搞那个《最佳实践》的一二两个都一直不行,找了好久找出来了。是这里那个 onCreate 参数用错了,改成用一个参数的就好了

    image-20220814170106325

  2. 然后把所有的 Activity 继承这个 BaseActivity

3.6.2 随时随地退出程序

image-20220716220857616

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
object ActivityCollector {
private val activities = ArrayList<Activity>()

fun addActivity(activity: Activity){
activities.add(activity)
}

fun removeActivity(activity: Activity){
activities.remove(activity)
}

fun finishAll(){
for (activity in activities){
if (!activity.isFinishing){
activity.finish()
}
activities.clear()

}
}
}
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("BaseActivity", javaClass.simpleName)
ActivityCollector.addActivity(this)
}
override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivity(this)
}
}



class ThirdActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("ThirdActivity", "Task id is $taskId")
setContentView(R.layout.third_layout)
button3.setOnClickListener {
ActivityCollector.finishAll() android.os.Process.killProcess(android.os.Process.myPid())//杀死当前进程
}
}
}

然后在之前那个 BaseActivity 的拆功

image-20220716221129901

3.6.3 启动Activity的最佳写法

养成习惯:写 Activity 的时候,每个前面都加一个

1
2
3
4
5
6
7
8
9
10
11
class SecondActivity : BaseActivity() {
...
companion object {
fun actionStart(context: Context, data1: String, data2: String) {
val intent = Intent(context, SecondActivity::class.java)
intent.putExtra("param1", data1)
intent.putExtra("param2", data2)
context.startActivity(intent)
}
}
}

然后大家就用这个来启动想要启动的 activity

四,UI

4.1 常用控件

4.1.1 TextView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?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">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="#00ff00"
android:textSize="25sp"
android:text="This is TextView"/>
<!-- 1.TextView 基础属性
在TextView中我们使用android:id给当前控件定义了一个
唯一标识符,这个属性在上一章中已经讲解过了。然后使用android:layout_width和
android:layout_height指定了控件的宽度和高度。Android中所有的控件都具有这两个属
性,可选值有3种:match_parent、wrap_content和固定值。match_parent表示让当前
控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小。wrap_content表
示让当前控件的大小能够刚好包含住里面的内容,也就是由控件内容决定当前控件的大小。固
定值表示表示给控件指定一个固定的尺寸,单位一般用dp,这是一种屏幕密度无关的尺寸单
位,可以保证在不同分辨率的手机上显示效果尽可能地一致,如50 dp就是一个有效的固定值。
-->
<!-- 2.文本居中
默认是左上角对齐
我们使用android:gravity来指定文字的对齐方式,可选值有top、bottom、start、
end、center等,可以用“|”来同时指定多个值,这里我们指定的是"center",效果等同
于"center_vertical|center_horizontal",表示文字在垂直和水平方向都居中对齐
-->
<!-- 3. 改字体大小颜色
通过android:textColor属性可以指定文字的颜色,通过android:textSize属性可以指定
文字的大小。文字大小要使用sp作为单位,这样当用户在系统中修改了文字显示尺寸时,应用
程序中的文字大小也会跟着变化。
-->
</LinearLayout>

4.1.2 Button

1
2
3
4
5
6
7
8
9
10
<Button
android:id="@+id/button02"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="I`m a button"
/>
<!--
可以在XML中添加android:textAllCaps="false"这个属性,这样系统就不会全部转为大写
-->

接口实现监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MainActivity : AppCompatActivity(),View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button01.setOnClickListener(this)
}

override fun onClick(p0: View?) {
when(p0?.id){
R.id.button02 -> {
//这里写点了 button 的逻辑
Toast.makeText(this,"大聪明,恭喜你会点按钮了",Toast.LENGTH_SHORT)
}
R.id.textView -> {
Toast.makeText(this,"点俺个文本干哈?",Toast.LENGTH_SHORT)
}
}
}
//这种办法写点击事件也有好处
}

函数式API实现监听器:

4.1.3 EditText

1
2
3
4
5
6
7
<EditText
android:id="@+id/edit_text01"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"
android:maxLines="3"
/>

可以配合Button玩

4.1.4 ImageView

在res目录下再新建一个drawable-xxhdpi目录

1
2
3
4
5
6
7
<ImageView
android:id="@+id/image_beauty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/img01"
android:layout_gravity="center"
/>

image-20220717171901651

4.1.5 ProgressBar

1
2
3
4
5
6
7
<ProgressBar
android:id="@+id/progress_bar01"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"
/>

控件的可见性,使用的是setVisibility()方法,允许传入View.VISIBLE、View.INVISIBLE和View.GONE这3种值。

1
2
3
4
5
6
7
8
9
10
R.id.button03 -> {
if (progress_bar01.visibility == View.VISIBLE){
progress_bar01.visibility = View.GONE
}else{
progress_bar01.visibility = View.VISIBLE
}
}
R.id.button04 -> {
progress_bar01.progress +=10
}

4.1.6 AlertDialog

1
2
3
4
5
6
7
8
9
R.id.button05 -> {
AlertDialog.Builder(this).apply {
setTitle("This is Dialog")
setMessage("Something important.")
setCancelable(false)
setPositiveButton("OK"){dialog,which ->}
setNegativeButton("Cannel"){dialog,which ->}
show()
}

4.2 布局

一个丰富的界面是由很多个控件组成的,布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。当然,布局的内部除了放置控件外,也可以放置布局,通过多层布局的嵌套,我们就能够完成一些比较复杂的界面实现。

4.2.1 LinearLayout

这个布局会将它所包含的控件在线性方向上依次排列

默认是横着的

布局和控件的关系 :

image-20220717195946819

android:gravity 用于指定文字在控件中的对齐方式,而 android:layout_gravity 用于指定控件在布局中的对齐方式

当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。因为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。

image-20220717200842675

android:layout_weight。这个属性允许我们使用比例的方式来指定控件的大小 就是权重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">www.blogss.cn
<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/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"
/>
</LinearLayout>

image-20220717201415694

4.2.2 RelativeLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button1"www.blogss.cn
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="Button 1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="Button 2" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:text="Button 4" />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="Button 5" />
</RelativeLayout>

image-20220718095917218

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3" />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"www.blogss.cn
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toLeftOf="@id/button3"
android:text="Button 1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toRightOf="@id/button3"
android:text="Button 2" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toLeftOf="@id/button3"
android:text="Button 4" />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toRightOf="@id/button3"
android:text="Button 5" />
</RelativeLayout>

注意,当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面,不然会出现找不到id的情况。

image-20220718100254802

RelativeLayout 中还有另外一组相对于控件进行定位的属性,android:layout_alignLeft 表示让一个控件的左边缘和另一个控件的左边缘对齐,android:layout_alignRight表示让一个控件的右边缘和另一个控件的右边缘对齐。此外,还有 android:layout_alignTop 和 android:layout_alignBottom,道理都是一样的,

4.3.3 FrameLayout

FrameLayout又称作帧布局,这种布局没有丰富的定位方式,所有的控件都会默认摆放在布局的左上角。 想要不堆在左上角就用 gravity中 left rigth 来指定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is TextView"
/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
/>
</FrameLayout>

image-20220815152712847

4.3 自定义控件

我们所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间
接继承自ViewGroup的。View是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩
形区域,并能响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View的基础上
又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多子View和子
ViewGroup,是一个用于放置控件和布局的容器。

常用控件和布局的继承结构 :

image-20220718101125140

4.3.1 引入布局

一般我们的程序中可能有很多个Activity需要这样的标题栏,如果在每个Activity的布局中都编写一遍同样的标题栏代码,明显就会导致代码的大量重复。这时我们就可以使用引入布局的方式来解决这个问题

  1. 在 layout 中定义一个 xml文件

    image-20220718104405612

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <?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="@color/teal_200">
    <Button
    android:id="@+id/titleBack"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_margin="5dp"
    android:background="@color/white"
    android:text="Back"
    android:textColor="@color/black"
    />
    <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="Title Text"
    android:textColor="@color/black"
    android:textSize="24sp" />
    <Button
    android:id="@+id/titleEdit"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_margin="5dp"
    android:background="@color/white"
    android:text="Edit"
    android:textColor="@color/black" />
    </LinearLayout>
  2. 注意:这里 的 include 和下面 自定义控件那个,只能有一个。不然会显示不出来。因为是继承关系撒

    image-20220718104540828

  3. image-20220718104628967

4.3.2 创建自定义控件

比如标题栏中的返回按钮,其实不管是在哪一个Activity中,这个按钮的功能都是相同的,即销毁当前 Activity。而如果在每一个Activity中都需要重新注册一遍返回按钮的点击事件,无疑会增加很多重复代码,这种情况最好是使用自定义控件的方式来解决

  1. 首先 新建一个类 TitleLayout继承刚我们写的自定义布局 LinearLayout,让它成为我们自定义的标题栏控件 ,写好自己的逻辑秩序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    package com.lmc.uicustomviews

    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 {
    /**
    * 注意,TitleLayout中接收的context参数实际上是一个Activity的实例,在返回按钮的点击事
    件里,我们要先将它转换成Activity类型,然后再调用finish()方法销毁当前的Activity。
    Kotlin中的类型强制转换使用的关键字是as,由于是第一次用到,所以这里单独讲解一下
    */
    val activity = context as Activity
    activity.finish()
    }
    titleEdit.setOnClickListener{
    Toast.makeText(context,"You clicked Edit button",Toast.LENGTH_SHORT).show()
    }
    }
    /**
    * 这里我们在TitleLayout的主构造函数中声明了Context和AttributeSet这两个参数,在布局中
    引入TitleLayout控件时就会调用这个构造函数。然后在init结构体中需要对标题栏布局进行动
    态加载,这就要借助LayoutInflater来实现了。通过LayoutInflater的from()方法可以构建出
    一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件。
    inflate()方法接收两个参数:第一个参数是要加载的布局文件的id,这里我们传入
    R.layout.title;第二个参数是给加载好的布局再添加一个父布局,这里我们想要指定为
    TitleLayout,于是直接传入this
    */
    }
  2. 在页面的布局文件中添加这个 自定义控件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.lmc.uicustomviews.TitleLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>



    <!--
    添加自定义控件和添加普通控件的方式基本是一样的,只不过在添加自定义控件的时候,我们
    需要指明控件的完整类名,包名在这里是不可以省略的 <include layout="@layout/title"/>
    -->0
    </LinearLayout>

    主意啊,这里有了辣个控件就不要再 include 辣个 layout title 了。因为这个控件中 LayoutInflater 相当于继承那个 布局了,这里直接这样就中

4.4 ListView

ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据会滚动出屏幕

4.4.1 简单用法

  1. 在 layout 界面写一个 ListView

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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">

    <ListView
    android:id="@+id/listView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />

    </LinearLayout>
  2. 在 MainActivity 中写逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.lmc.listviewtest

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

    class MainActivity : AppCompatActivity() {
    private val data = listOf("Apple", "Banana", "Orange", "Watermelon",
    "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
    "Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape",
    "Pineapple", "Strawberry", "Cherry", "Mango")
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val adapter = ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)
    listView.adapter = adapter
    }
    }

    其中 ListView 是不能直接接收对象的,需要个适配器。其中最好用的是 ArrayAdapter.他需要传三个参数:当前Avtivity的上下文,ListView子类布局是什么样子,数据源。其中android.R.layout.simple_list_item_1作为ListView子项布局的id,这是一个
    Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本。

    而且这里通过泛型来指定适配的数据类型,因为我们是字符串所以我们直接用String

4.4.2 定制 ListView

  1. 准备图片资源image-20220719221720549

  2. 写 ListView 适配器的适配类型image-20220719222250452

  3. 写 ListView 的子项是是什么布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?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="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>

    在这个布局中,我们定义了一个ImageView用于显示水果的图片,又定义了一个TextView用于显示水果的名称,并让ImageView和TextView都在垂直方向上居中显示

  4. 写自定义适配器:!!!(难点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package com.lmc.android19.listview

    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
    import com.lmc.android19.R

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

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    //
    val view = LayoutInflater.from(context).inflate(resourceId, parent, false)
    val fruitImage = view.findViewById<ImageView>(R.id.fruitImage)
    val fruitName = view.findViewById<TextView>(R.id.fruitName)
    val fruit = getItem(position)//获取当前项的Fruit实例
    if (fruit != null) {
    fruitImage.setImageResource(fruit.imageId)
    fruitName.text = fruit.name
    }
    return view
    }

    }

    FruitAdapter定义了一个主构造函数,用于将Activity的实例、ListView子项布局的id和数据源传递进来。另外又重写了getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。

    在getView()方法中,首先使用LayoutInflater来为这个子项加载我们传入的布局。LayoutInflater的inflate()方法接收3个参数,前两个参数我们已经知道是什么意思了,第三个参数指定成false,表示只让我们在父布局中声明的layout属性生效,但不会为这个View添加父布局。因为一旦View有了父布局之后,它就不能再添加到ListView中了。如果你现在还不能理解这段话的含义,也没关系,只需要知道这是ListView中的标准写法就可以了,当你以后对View理解得更加深刻的时候,再来读这段话就没有问题了。我们继续往下看,接下来调用View的findViewById()方法分别获取到ImageView和TextView的实例,然后通过getItem()方法得到当前项的Fruit实例,并分别调用它们的setImageResource()和setText()方法设置显示的图片和文字,最后将布局返回,这样我们自定义的适配器就完成了。

  5. 修改 MainActivity 中的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    package com.lmc.android19.listview

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.widget.ArrayAdapter
    import com.lmc.android19.R
    import com.lmc.android19.databinding.ActivityMainBinding

    class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val fruitList = ArrayList<Fruit>()

    fun initBinding() {
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    initBinding()
    initFruits()
    binding.listView.adapter =
    FruitAdapter(this, R.layout.fruit_item, fruitList)
    }
    private fun initFruits(){
    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("Pear", R.drawable.pear_pic))
    fruitList.add(Fruit("Grape", R.drawable.grape_pic))
    fruitList.add(Fruit("Pineapple", R.drawable.pineapple_pic))
    fruitList.add(Fruit("Strawberry", R.drawable.strawberry_pic))
    fruitList.add(Fruit("Cherry", R.drawable.cherry_pic))
    fruitList.add(Fruit("Mango", R.drawable.mango_pic))
    }
    }
    }

4.4.3 提高 ListView 运行效率

目前我们ListView的运行效率是很低的,因为在FruitAdapter的getView()方法中,每次都将布局重新加载了一遍,当ListView快速滚动的时候,这就会成为性能的瓶颈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.lmc.listviewtest

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
import kotlinx.android.synthetic.main.fruit_item.view.*


//原始版本
//class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
// ArrayAdapter<Fruit>(activity, resourceId, data) {
// override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
// val view = LayoutInflater.from(context).inflate(resourceId, parent, false)
// val fruitImage: ImageView = view.fruitImage
// val fruitName: TextView = view.fruitName
// val fruit = getItem(position) // 获取当前项的Fruit实例
// if (fruit != null) {
// fruitImage.setImageResource(fruit.imageId)
// fruitName.text = fruit.name
// }
// return view
// }
//}

//优化1 convertView 布局缓存
//class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
// ArrayAdapter<Fruit>(activity, resourceId, data) {
// override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
// val view: View
// if (convertView == null) {
// view = LayoutInflater.from(context).inflate(resourceId, parent, false)
// } else {
// view = convertView
// }
// val fruitImage: ImageView = view.fruitImage
// val fruitName: TextView = view.fruitName
// val fruit = getItem(position) // 获取当前项的Fruit实例
// if (fruit != null) {
// fruitImage.setImageResource(fruit.imageId)
// fruitName.text = fruit.name
// }
// return view
// }
//}
/**
* 可以看到,现在我们在getView()方法中进行了判断:如果convertView为null,则使用
LayoutInflater去加载布局;如果不为null,则直接对convertView进行重用。这样就大
大提高了ListView的运行效率,在快速滚动的时候可以表现出更好的性能。
*/

//优化2 ViewHolder 控件缓存

class FruitAdapter(activity: Activity, 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: ImageView = view.fruitImage
val fruitName: TextView = view.fruitName
viewHolder = ViewHolder(fruitImage, fruitName)
view.tag = viewHolder
} else {
view = convertView
viewHolder = view.tag as ViewHolder
}
val fruit = getItem(position) // 获取当前项的Fruit实例
if (fruit != null) {
viewHolder.fruitImage.setImageResource(fruit.imageId)
viewHolder.fruitName.text = fruit.name
}
return view
}
}
/**
* 我们新增了一个内部类ViewHolder,用于对ImageView和TextView的控件实例进行缓存,
Kotlin中使用inner class关键字来定义内部类。当convertView为null的时候,创建一个
ViewHolder对象,并将控件的实例存放在ViewHolder里,然后调用View的setTag()方
法,将ViewHolder对象存储在View中。当convertView不为null的时候,则调用View的
getTag()方法,把ViewHolder重新取出。这样所有控件的实例都缓存在了ViewHolder里,
就没有必要每次都通过findViewById()方法来获取控件实例了。
*/

4.4.4 ListView 点击事件

1
2
3
4
5
//下面是加上点击效果
listView.setOnItemClickListener { _, _, position, _ ->
val fruit = fruitList[position]
Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show()
}

4.5 RecyclerView

4.5.1 基本用法

  1. app:build.gradle 中加一行依赖关系

    1
    implementation 'androidx.recyclerview:recyclerview:1.2.1'//RecycleView 控件
  2. 修改 activity_main.xml 中的代码

    ViewHolder的主构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外层布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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">

    <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

    </LinearLayout>
  3. 准备适配器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package com.lmc.recyclerviewtest

    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
    import com.lmc.listviewtest.Fruit

    class FruitAdapter(val fruitList: List<Fruit>) :
    RecyclerView.Adapter<FruitAdapter.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
    }
  4. 改 MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    package com.lmc.recyclerviewtest

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import androidx.recyclerview.widget.LinearLayoutManager
    import com.lmc.listviewtest.Fruit
    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 = FruitAdapter(fruitList)
    recyclerView.adapter = adapter
    }
    private fun initFruits() {
    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("Pear", R.drawable.pear_pic))
    fruitList.add(Fruit("Grape", R.drawable.grape_pic))
    fruitList.add(Fruit("Pineapple", R.drawable.pineapple_pic))
    fruitList.add(Fruit("Strawberry", R.drawable.strawberry_pic))
    fruitList.add(Fruit("Cherry", R.drawable.cherry_pic))
    fruitList.add(Fruit("Mango", R.drawable.mango_pic))
    }
    }
    }

4.5.2 横向滚动/瀑布式布局

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

横向滚动

  1. 修改 fruit_item.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <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_marginTop="10dp" />
    <TextView
    android:id="@+id/fruitName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:layout_marginTop="10dp" />
    </LinearLayout>
  2. 改 MainActivity

    1
    layoutManager.orientation = LinearLayoutManager.HORIZONTAL//加上这个横着显示 加在 onCreate 里面,一行代码就行

瀑布式布局

  1. 修改 fruit_item.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <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_marginTop="10dp" />
    <TextView
    android:id="@+id/fruitName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="left"
    android:layout_marginTop="10dp" />
    </LinearLayout>
  2. 修改 MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    package com.lmc.recyclerviewtest

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import androidx.recyclerview.widget.LinearLayoutManager
    import androidx.recyclerview.widget.StaggeredGridLayoutManager
    import com.lmc.listviewtest.Fruit
    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)//这里换成了瀑布布局
    layoutManager.orientation = LinearLayoutManager.HORIZONTAL//加上这个横着显示
    recyclerView.layoutManager = layoutManager
    val adapter = FruitAdapter(fruitList)
    recyclerView.adapter = adapter
    }
    private fun initFruits() {
    repeat(4) {
    fruitList.add(Fruit(getRandomLengthString("Apple"),
    R.drawable.apple_pic))
    fruitList.add(Fruit(getRandomLengthString("Banana"),
    R.drawable.banana_pic))
    fruitList.add(Fruit(getRandomLengthString("Orange"),
    R.drawable.orange_pic))
    fruitList.add(Fruit(getRandomLengthString("Watermelon"),
    R.drawable.watermelon_pic))
    fruitList.add(Fruit(getRandomLengthString("Pear"),
    R.drawable.pear_pic))
    fruitList.add(Fruit(getRandomLengthString("Grape"),
    R.drawable.grape_pic))
    fruitList.add(Fruit(getRandomLengthString("Pineapple"),
    R.drawable.pineapple_pic))
    fruitList.add(Fruit(getRandomLengthString("Strawberry"),
    R.drawable.strawberry_pic))
    fruitList.add(Fruit(getRandomLengthString("Cherry"),
    R.drawable.cherry_pic))
    fruitList.add(Fruit(getRandomLengthString("Mango"),
    R.drawable.mango_pic))
    }
    }
    private fun getRandomLengthString(str: String): String {
    val n = (1..20).random()
    val builder = StringBuilder()
    repeat(n) {
    builder.append(str)
    }
    return builder.toString()
    }
    }

4.5.3 点击事件

  1. 修改 FruitAdapter

    image-20220817210444863

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    package com.lmc.android20

    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

    //为 RecyclerView 准备一个适配器
    class FruitAdapter(val fruitList: List<Fruit>):
    RecyclerView.Adapter<FruitAdapter.ViewHolder>(){
    //ViewHolder的主构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外层布局
    inner class ViewHolder(view: View):RecyclerView.ViewHolder(view){
    //通过findViewById()方法来获取布局中ImageView和TextView的实例
    val fruitImage = view.findViewById<ImageView>(R.id.fruitImage)
    val fruitName = view.findViewById<TextView>(R.id.fruitName)
    }
    //用于创建ViewHolder实例
    /**
    * 将fruit_item布局加载进来,然后创建一个ViewHolder实例,并把加载出来的布局传入构造
    函数当中,最后将ViewHolder的实例返回。
    */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitAdapter.ViewHolder {
    val view = LayoutInflater.from(parent.context)
    .inflate(R.layout.fruit_item,parent,false)
    //我是从这里开始点击事件的编写的
    val viewHolder = ViewHolder(view)
    viewHolder.itemView.setOnClickListener{
    val position = viewHolder.bindingAdapterPosition
    val fruit = fruitList[position]
    Toast.makeText(parent.context,"you clicked view ${fruit.name}",Toast.LENGTH_SHORT).show()
    }
    viewHolder.fruitImage.setOnClickListener {
    val position = viewHolder.bindingAdapterPosition
    val fruit = fruitList[position]
    Toast.makeText(parent.context,"you clicked image ${fruit.name}",Toast.LENGTH_SHORT).show()
    }
    return viewHolder
    }
    //用于对 RecyclerView子项的数据进行赋值,会在每个子项被滚动到屏幕内的时候执行
    override fun onBindViewHolder(holder: FruitAdapter.ViewHolder, position: Int) {
    val fruit = fruitList[position]
    holder.fruitImage.setImageResource(fruit.imageId)
    holder.fruitName.text = fruit.name
    }
    //一共有多少子项
    override fun getItemCount() = fruitList.size

    }

4.6 实践

4.6.1 制作9-Patch图片

一种被特殊处理过的png图片,能够指定哪些区域可以被拉伸、哪些区域不可以。

image-20220720214836410

我们可以在图片的4个边框绘制一个个的小黑点,在上边框和左边框绘制的部分表示当图片需要拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分表示内容允许被放置的区域。使用鼠标在图片的边缘拖动就可以进行绘制了,按住Shift键拖动可以进行擦除。

4.6.2 写聊天界面

  1. 加依赖:

    1
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
  2. 写 activity_main.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#d8e0e8"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"/>
    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <EditText
    android:id="@+id/inputText"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:hint="写点啥吧"
    android:maxLines="2"/>
    <Button
    android:id = "@+id/send"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Send"/>
    </LinearLayout>



    </LinearLayout>
  3. 定义消息实体类

    1
    2
    3
    4
    5
    6
    7
    8
    package com.lmc.android21

    class Msg(val content:String,val type:Int) {
    companion object {
    const val TYPE_RECEIVED = 0
    const val TYPE_SENT = 1
    }
    }

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

  4. 写 RecycleView 子布局

    这是接收消息的子项布局。这里我们让收到的消息居左对齐,并使 message_left.9.png作为背景图 :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">
    <LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="left"
    android:background="@drawable/message_left">
    <TextView
    android:id="@+id/leftMsg"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_margin="10dp"
    android:textColor="#fff"/>
    </LinearLayout>

    </FrameLayout>

    发送消息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="10dp">

    <LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="right"
    android:background="@drawable/message_right">

    <TextView
    android:id="@+id/rightMsg"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_margin="10dp"
    android:textColor="#000" />
    </LinearLayout>
    </FrameLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.lmc.android21

import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

sealed class MsgViewHolder(view: View):RecyclerView.ViewHolder(view)
class LeftViewHolder(view: View):MsgViewHolder(view){
val leftMsg = view.findViewById<TextView>(R.id.leftMsg)
}
class RightViewHolder(view: View):MsgViewHolder(view){
val rightMsg:TextView = view.findViewById(R.id.rightMsg)
}
  1. 写 RecyclerView 适配器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    package com.lmc.android21

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

    class MsgAdapter(val msgList: List<Msg>):RecyclerView.Adapter<RecyclerView.ViewHolder>() {


    //返回当前position对应的消息类型
    override fun getItemViewType(position: Int): Int {
    val msg = msgList[position]
    return msg.type
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = if (viewType == Msg.TYPE_RECEIVED){
    val view = LayoutInflater.from(parent.context).inflate(R.layout.msg_left_item,parent,false)
    LeftViewHolder(view)
    }else{
    val view = LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item,parent,false)
    RightViewHolder(view)
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    val msg = msgList[position]
    when(holder){
    is LeftViewHolder -> holder.leftMsg.text = msg.content
    is RightViewHolder -> holder.rightMsg.text = msg.content
    }
    }

    override fun getItemCount() = msgList.size
    }
  2. 修改 MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    package com.lmc.android21

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

    class MainActivity : AppCompatActivity(), View.OnClickListener {
    private val msgList = ArrayList<Msg>()
    private lateinit var adapter: MsgAdapter
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    initMsg()
    recyclerView.layoutManager = LinearLayoutManager(this)
    if (!::adapter.isInitialized) {
    adapter = MsgAdapter(msgList)
    }
    recyclerView.adapter = adapter
    send.setOnClickListener(this)
    }

    private fun initMsg() {
    val msg1 = Msg("Hello guy.", Msg.TYPE_RECEIVED)
    msgList.add(msg1)
    val msg2 = Msg("Who is that?", Msg.TYPE_SENT)
    msgList.add(msg2)
    val msg3 = Msg("This is LMC. Nice talking to you.", Msg.TYPE_RECEIVED)
    msgList.add(msg3)
    }

    override fun onClick(v: View?) {
    when (v) {
    send -> {
    val content = inputText.text.toString()
    if (content.isNotEmpty()) {
    val msg = Msg(content, Msg.TYPE_SENT)
    msgList.add(msg)
    adapter.notifyItemInserted(msgList.size - 1)//当有新消息时,刷新 RecyclerView 来显示
    recyclerView.scrollToPosition(msgList.size - 1)//将 RecycclerView 定位到最后一行
    inputText.setText("")//清空输入框中的内容
    }
    }
    }

    }

    }

Screenshot_20220818_195702_com.lmc.android21

Screenshot_20220818_195706_com.lmc.android21

五,Fragment

Fragment是一种可以嵌入在Activity当中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间,因而在平板上应用得非常广泛。

平板双页设计:

image-20220827203559531

5.1 使用方法

用这个代替《第一行代码》第三版中老师说的那个AS自带虚拟机

传送门:https://www.bilibili.com/video/BV1hG4y1r7fM/

5.1.1 简单用法

  1. 新建左右两个 xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <?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>


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:background="#00ff00"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <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>
  2. 新建左右两个对应的类

    注意每个类继承的都是 AndroidX库下面的,别用系统库的。

    因为能保证各个 Android版本一致,系统那个已经被废弃了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    package com.lmc.fragmenttest

    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)
    }
    }


    package com.lmc.fragmenttest

    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)
    }
    }
  3. 修改 activity_main.xml 文件,引入两个 Fragment

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
    android:id="@+id/leftFrag"
    android:name="com.lmc.fragmenttest.LeftFragment"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1" />
    <fragment
    android:id="@+id/rightFrag"
    android:name="com.lmc.fragmenttest.RightFragment"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1" />

    </LinearLayout>

    只不过这里还需要通过android:name属性来显式声明要添加的Fragment类
    名,注意一定要将类的包名也加上。

5.1.2 动态添加 Fragment

  1. 新建 another_right_fragment.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:background="#ffff00"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:textSize="24sp"
    android:text="This is another right fragment"
    />

    </LinearLayout>
  2. 新建 AnotherRightFragment

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package com.lmc.fragmenttest

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

    class AnotherRightFragment:Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.another_right_fragment, container, false)
    }
    }
  3. 改动 activity_main_xml 右侧Fragment替换成了一个FrameLayout。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
    android:id="@+id/leftFrag"
    android:name="com.lmc.fragmenttest.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" >
    </FrameLayout>


    </LinearLayout>
  4. 下面我们将在代码中向FrameLayout里添加内容,从而实现动态添加Fragment的功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.lmc.fragmenttest

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    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(AnotherRightFragment())
    }
    replaceFragment(RightFragment())
    }

    private fun replaceFragment(fragment: Fragment) {
    val fragmentManager = supportFragmentManager
    val transaction = fragmentManager.beginTransaction()
    transaction.replace(R.id.rightLayout, fragment)
    transaction.commit()
    }
    }

    首先我们给左侧Fragment中的按钮注册了一个点击事件,然后调用
    replaceFragment()方法动态添加了RightFragment。当点击左侧Fragment中的按钮时,
    又会调用replaceFragment()方法,将右侧Fragment替换成AnotherRightFragment。结
    合replaceFragment()方法中的代码可以看出,动态添加Fragment主要分为5步。
    (1) 创建待添加Fragment的实例。
    (2) 获取FragmentManager,在Activity中可以直接调用getSupportFragmentManager()
    方法获取。
    (3) 开启一个事务,通过调用beginTransaction()方法开启。
    (4) 向容器内添加或替换Fragment,一般使用replace()方法实现,需要传入容器的id和待添
    加的Fragment实例。
    (5) 提交事务,调用commit()方法来完成。

5.1.3 在 Fragment 中实现返回栈

image-20220721145858203

1
transaction.addToBackStack(null)//加上这个让出现了动态画面然后返回是回到点击之前,而不是直接退出

5.1.4 Fragment和Activity之间的交互

5.1.4.1 如何在Activity中调用Fragment里的方法
  1. 像这样获得 Fragment 的实例

    val fragment = leftFrag as LeftFragment

  2. 然后直接用 这个变量的方法就可以的

5.1.4.2 Fragment中又该怎样调用Activity
1
2
3
if (activity != null) {
val mainActivity = activity as MainActivity
}

既然Fragment和Activity之间的通信问题已经解决了,那么不同的Fragment之间可不可以进行通信呢?说实在的,这个问题并没有看上去那么复杂,它的基本思路非常简单:首先在一个Fragment中可以得到与它相关联的Activity,然后再通过这个Activity去获取另外一个Fragment的实例,这样就实现了不同Fragment之间的通信功能。因此,这里我们的回答是肯定的。

5.2 Fragment的生命周期

5.2.1 Fragment的状态和回调

状态
  1. 运行状态
    当一个Fragment所关联的Activity正处于运行状态时,该Fragment也处于运行状态。
  2. 暂停状态
    当一个Activity进入暂停状态时(由于另一个未占满屏幕的Activity被添加到了栈顶),与它相关联的Fragment就会进入暂停状态。
  3. 停止状态
    当一个Activity进入停止状态时,与它相关联的Fragment就会进入停止状态,或者通过调用FragmentTransaction的remove()、replace()方法将Fragment从Activity中移除,但在事务提交之前调用了addToBackStack()方法,这时的Fragment也会进入停止
    状态。总的来说,进入停止状态的Fragment对用户来说是完全不可见的,有可能会被系统回收。
  4. 销毁状态
    Fragment总是依附于Activity而存在,因此当Activity被销毁时,与它相关联的 Fragment就会进入销毁状态。或者通过调用FragmentTransaction的remove()、replace()方法将Fragment从Activity中移除,但在事务提交之前并没有调用addToBackStack()方法,这时的Fragment也会进入销毁状态。
回调
  • onAttach():当Fragment和Activity建立关联时调用。
  • onCreateView():为Fragment创建视图(加载布局)时调用。
  • onActivityCreated():确保与Fragment相关联的Activity已经创建完毕时调用。
  • onDestroyView():当与Fragment关联的视图被移除时调用。
  • onDetach():当Fragment和Activity解除关联时调用。

image-20220721171753694

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package com.lmc.a23

import android.content.Context
import android.nfc.Tag
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment

class RightFragment: Fragment() {
companion object {
const val TAG = "RightFragment"
}
//绑定 Fragment 和关联的 Activity
override fun onAttach(context: Context) {
super.onAttach(context)
Log.d(TAG,"onAttach")
}
//创建 Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG,"onCreate")
}
//创建 Fragment 的视图
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.d(TAG,"onCreateView")
return inflater.inflate(R.layout.right_item,container,false)
}
//确认 Activity 创建完毕
//《第一行代码》第三版 这里用的是 onActivityCreated() 现已弃用
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d(TAG,"onViewCreated")
}


//Activity 能看到
override fun onStart() {
super.onStart()
Log.d(TAG,"onStart")
}
//Activity 能交互
override fun onResume() {
super.onResume()
Log.d(TAG,"onResume")
}



//暂停 不能交互
override fun onPause() {
super.onPause()
Log.d(TAG,"onPause")
}
//停止 看不见
override fun onStop() {
super.onStop()
Log.d(TAG,"onStop")
}
//Fragment 视图没了
override fun onDestroyView() {
super.onDestroyView()
Log.d(TAG,"onDestroyView")
}
//Activity 被销毁
override fun onDestroy() {
super.onDestroy()
Log.d(TAG,"onDestroy")
}
//Activity 和 Fragment 取消绑定
override fun onDetach() {
super.onDetach()
Log.d(TAG,"onDetach")
}

}
  1. 由于AnotherRightFragment替换了RightFragment,此时的RightFragment进入了停止状态,因此onPause()、onStop()和onDestroyView()方法会得到执行。当然,如果在替换的时候没有调用addToBackStack()方法,此时的RightFragment就会进入销毁状态,onDestroy()和onDetach()方法就会得到执行

  2. 另外值得一提的是,在Fragment中你也可以通过onSaveInstanceState()方法来保存数据,因为进入停止状态的Fragment有可能在系统内存不足的时候被回收。保存下来的数据在 onCreate()、onCreateView()和onActivityCreated()这3个方法中你都可以重新得到,它们都含有一个Bundle类型的savedInstanceState参数。具体的代码我就不在这里展示了,如果你忘记了该如何编写,可以参考3.4.5小节

5.3 动态加载布局的技巧

程序能够根据设备的分辨率或屏幕大小,在运行时决定加载哪个布局

注意:下面内容受手机平板虚拟机的尺寸差异过大很可能你的代码是正确的但是运行起来得不到预期效果,其实是设备的尺寸问题

5.3.1 使用限定符

  1. 将 activity_main.xml文件 中只有一个 Fragment

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <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/leftFrag"
    android:name="com.example.fragmenttest.LeftFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    </LinearLayout>
  2. 在 res 目录下面新建 layout-large 文件夹 ,这下面做一个 activity_main.xml 文件。里面写两个 Fragment

    注意:!!!这里 layout-large中间的小杠在中间鸭,别写成了layout_large

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <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/leftFrag"
    android:name="com.example.fragmenttest.LeftFragment"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1" />
    <fragment
    android:id="@+id/rightFrag"
    android:name="com.example.fragmenttest.RightFragment"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="3" />
    </LinearLayout>
  3. 可以看到,layout/activity_main布局只包含了一个Fragment,即单页模式,而layoutlarge/ activity_main布局包含了两个Fragment,即双页模式。其中,large就是一个限定符,那些屏幕被认为是large的设备就会自动加载layout-large文件夹下的布局,小屏幕的设备则还是会加载layout文件夹下的布局

    image-20220721174847116image-20220721174855977

  4. image-20220721175026965

    image-20220721175033370

5.4.2 使用最小宽度限定符

最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。

image-20220721203429860

bandicam 2022-08-29 11-04-13-387

5.4 Fragment 实战 新闻页面

  1. 加上那个 kotlin-android-extensions插件和 RecycleView 的插件

  2. 写 新闻 实体类

    1
    2
    3
    package com.lmc.android26

    class News(val title:String,val content:String)
  3. 写 news_content_frag.xml 新闻内容布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
    android:id="@+id/contentLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:visibility="invisible">
    <TextView
    android:id="@+id/newsTitle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:padding="10dp"
    android:textSize="20sp"/>
    <View
    android:layout_width="match_parent"
    android:layout_height="1dp"
    android:background="#000"/>
    <TextView
    android:id="@+id/newsContent"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:padding="15dp"
    android:textSize="18sp"/>
    </LinearLayout>
    <View
    android:layout_width="1dp"
    android:layout_height="match_parent"
    android:layout_alignParentStart="true"
    android:background="#000"/>
    </RelativeLayout>
  4. 写 NewsContentFragment

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.lmc.fragmentbestpractice

    import android.os.Bundle
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import androidx.fragment.app.Fragment
    import kotlinx.android.synthetic.main.news_content_frag.*

    class NewsContentFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.news_content_frag, container, false)
    }
    fun refresh(title: String, content: String) {
    contentLayout.visibility = View.VISIBLE
    newsTitle.text = title // 刷新新闻的标题
    newsContent.text = content // 刷新新闻的内容
    }
    }
  5. 为了单页面中也能用,加一个 NewsContentActivity 。然后修改 activity_news_content.xml 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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"
    android:orientation="vertical"
    tools:context=".NewsContentActivity">
    <fragment
    android:id="@+id/newsContentFrag"
    android:name="com.lmc.android26.NewsContentFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

    </LinearLayout>

    这里我们充分发挥了代码的复用性,直接在布局中引入了NewsContentFragment。这样相当于把news_content_frag布局的内容自动加了进来。

  6. 然后再写 NewsContentActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    package com.lmc.android26

    import android.content.Context
    import android.content.Intent
    import android.os.Bundle
    import android.util.Log
    import androidx.appcompat.app.AppCompatActivity
    import kotlinx.android.synthetic.main.activity_news_content.*

    class NewsContentActivity : AppCompatActivity() {

    companion object {
    fun actionStart(context: Context, title: String, content: String){
    val intent = Intent(context,NewsContentActivity::class.java).apply {
    putExtra("news_title",title)
    putExtra("news_content",content)
    }
    context.startActivity(intent)
    }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_news_content)
    val title = intent.getStringExtra("news_title")//获取传入的新闻标题
    Log.d("NB","title is $title")
    val context = intent.getStringExtra("news_content")//获取传入的新闻内容
    Log.d("NB","context is $context")
    //这里显示不了内容,所以注释掉判断条件看一下
    //2022/9/3/20:27 title 有值 context 是null
    //原因:上面那个news_content少了一个s

    if (title != null && context != null){
    //检查到这里,这里有一个空值
    Log.d("NB","我是NewsContentActivity中的那个onCreate方法,进来这里说明拿到的title和content不是空的")
    val fragment = newsContentFrag as NewsContentFragment
    fragment.refresh(title,context)//刷新NewsContentFragment界面
    }


    }
    }
  7. 接下来还需要再创建一个用于显示新闻列表的布局,新建news_title_frag.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?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">
    <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/newsTitleRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    </LinearLayout>
  8. 新闻列表子项布局

    android:padding表示给控件的周围加上补白,这样不至于让文本内容紧靠在边缘上;android:maxLines设置为1表示让这个TextView只能单行显示;android:ellipsize用于设定当文本内容超出控件宽度时文本的缩略方式,这里指定成end表示在尾部进行缩略

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?xml version="1.0" encoding="utf-8"?>
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/newsTitle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:maxLines="1"
    android:ellipsize="end"
    android:textSize="18sp"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:paddingTop="15dp"
    android:paddingBottom="15dp"/>

  9. 一个用于展示新闻列表的地方。这里新建NewsTitleFragment作为展示新闻列表的Fragment

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.lmc.fragmentbestpractice

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

    class NewsTitleFragment : Fragment() {
    private var isTwoPane = false
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.news_title_frag, container, false)
    }
    override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    isTwoPane = activity?.findViewById<View>(R.id.newsContentLayout) != null
    }
    }
  10. 修改 activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/newsTitleLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/newsTitleFrag"
android:name="com.lmc.android26.NewsTitleFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</FrameLayout>
  1. 新建layout-sw600dp文件夹,在这个文件夹下再新建一个activity_main.xml文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <?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/newsTitleFrag"
    android:name="com.lmc.android26.NewsTitleFragment"
    android:layout_width="0dp"
    android:layout_weight="1"
    android:layout_height="match_parent"/>
    <FrameLayout
    android:id="@+id/newsContentLayout"
    android:layout_width="0dp"
    android:layout_weight="3"
    android:layout_height="match_parent" >
    <fragment
    android:id="@+id/newsContentFrag"
    android:name="com.lmc.android26.NewsContentFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    </FrameLayout>
    </LinearLayout>

    image-20220721212530865

  2. 在NewsTitleFragment 中新建一个内部类NewsAdapter来作为RecyclerView的适配器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    package com.lmc.fragmentbestpractice

    import android.os.Bundle
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.TextView
    import androidx.fragment.app.Fragment
    import androidx.recyclerview.widget.RecyclerView
    import kotlinx.android.synthetic.main.activity_news_content.*

    class NewsTitleFragment : Fragment() {
    private var isTwoPane = false
    inner class NewsAdapter(val newsList: List<News>) :
    RecyclerView.Adapter<NewsAdapter.ViewHolder>() {
    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val newsTitle: TextView = view.findViewById(R.id.newsTitle)
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val view = LayoutInflater.from(parent.context)
    .inflate(R.layout.news_item, parent, false)
    val holder = ViewHolder(view)
    holder.itemView.setOnClickListener {
    val news = newsList[holder.bindingAdapterPosition]
    if (isTwoPane) {
    // 如果是双页模式,则刷新NewsContentFragment中的内容
    val fragment = newsContentFrag as NewsContentFragment
    fragment.refresh(news.title, news.content)
    } else {
    // 如果是单页模式,则直接启动NewsContentActivity
    NewsContentActivity.actionStart(parent.context, news.title,
    news.content)
    }
    }
    return holder
    }
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val news = newsList[position]
    holder.newsTitle.text = news.title
    }
    override fun getItemCount() = newsList.size
    }
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.news_title_frag, container, false)
    }
    override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    isTwoPane = activity?.findViewById<View>(R.id.newsContentLayout) != null
    }
    }
  3. 向RecyclerView中填充数据了。修改NewsTitleFragment 中的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    package com.lmc.fragmentbestpractice

    import android.os.Bundle
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.TextView
    import androidx.fragment.app.Fragment
    import androidx.recyclerview.widget.LinearLayoutManager
    import androidx.recyclerview.widget.RecyclerView
    import kotlinx.android.synthetic.main.activity_news_content.*
    import kotlinx.android.synthetic.main.news_title_frag.*

    class NewsTitleFragment : Fragment() {
    private var isTwoPane = false
    inner class NewsAdapter(val newsList: List<News>) :
    RecyclerView.Adapter<NewsAdapter.ViewHolder>() {
    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val newsTitle: TextView = view.findViewById(R.id.newsTitle)
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val view = LayoutInflater.from(parent.context)
    .inflate(R.layout.news_item, parent, false)
    val holder = ViewHolder(view)
    holder.itemView.setOnClickListener {
    val news = newsList[holder.adapterPosition]
    if (isTwoPane) {
    // 如果是双页模式,则刷新NewsContentFragment中的内容
    val fragment = newsContentFrag as NewsContentFragment
    fragment.refresh(news.title, news.content)
    } else {
    // 如果是单页模式,则直接启动NewsContentActivity
    NewsContentActivity.actionStart(parent.context, news.title,
    news.content)
    }
    }
    return holder
    }
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val news = newsList[position]
    holder.newsTitle.text = news.title
    }
    override fun getItemCount() = newsList.size
    }



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

    override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    isTwoPane = activity?.findViewById<View>(R.id.newsContentLayout) != null
    val layoutManager = LinearLayoutManager(activity)
    newsTitleRecyclerView.layoutManager = layoutManager
    val adapter = NewsAdapter(getNews())
    newsTitleRecyclerView.adapter = adapter
    }
    private fun getNews(): List<News> {
    val newsList = ArrayList<News>()
    for (i in 1..50) {
    val news = News("This is news title $i", getRandomLengthString("This is news content $i. "))
    newsList.add(news)
    }
    return newsList
    }
    private fun getRandomLengthString(str: String): String {
    val n = (1..20).random()
    val builder = StringBuilder()
    repeat(n) {
    builder.append(str)
    }
    return builder.toString()
    }
    }

Fragment实践《聊天页面Demo》

上图分享ppt/PDF地址:

https://wwc.lanzoum.com/b03dcu3ob
密码:f5ht

六,广播机制

6.1 广播机制简介

BroadcastReceiver

广播类型

  1. 标准广播(normal broadcasts)

    异步执行

    image-20220722181607244

    同一时间全给出去了,拦截不了,但效率高.

  2. 有序广播(ordered broadcasts)

    同步执行

    image-20220722182131455

6.2 接受系统广播

啥叫 系统广播?

Android内置了很多系统级别的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。比如手机开机完成后会发出一条广播,电池的电量发生变化会发出一条广播,系统时间发生改变也会发出一条广播,等等。

6.2.1 动态注册监听时间变化

注册 BroadcastReceiver的方式一般有两种:在代码中注册和在AndroidManifest.xml中注册。其中前者也被称为动态注册,后者也被称为静态注册。

如何创建一个BroadcastReceiver呢?

其实只需新建一个类,让它继承自 BroadcastReceiver,并重写父类的 onReceive() 方法就行了。这样当有广播到来时,onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。

动态注册的BroadcastReceiver一定要取消注册才行,这里我们是在 onDestroy()方法中通过调用unregisterReceiver()方法来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.lmc.android28

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
//实现一个BroadcastReceiver的通知监听器,负责监听系统的时间变化通知
class MainActivity : AppCompatActivity() {
lateinit var timeChangeReceiver:TimeChangeReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

timeChangeReceiver = TimeChangeReceiver()
registerReceiver(timeChangeReceiver,IntentFilter("android.intent.action.TIME_TICK"))
}

override fun onDestroy() {
super.onDestroy()
unregisterReceiver(timeChangeReceiver)
}

inner class TimeChangeReceiver:BroadcastReceiver(){
override fun onReceive(p0: Context?, p1: Intent?) {
Toast.makeText(p0,"时间改变了",Toast.LENGTH_SHORT).show()
}

}
}

完整广播列表地址:

/platforms/<任意android api版本>/data/broadcast_actions.txt

6.2.2 静态注册实现开机启动

动态注册的BroadcastReceiver可以自由地控制注册与注销,在灵活性方面有很大的优势。但是它存在着一个缺点,即必须在程序启动之后才能接收广播,因为注册的逻辑是写在 onCreate()方法中的。

静态注册:可以让程序在未启动的情况下也能接收广播

在Android 8.0系统之后,所有隐式广播都不允许使用静态注册的方式来接收了。隐式广播指的是那些没有具体指定发送给哪个应用程序的广播,大多数系统广播属于隐式广播,但是少数特殊的系统广播目前仍然允许使用静态注册的方式来接收。这些特殊的系统广播列表详见
https://developer.android.google.cn/guide/components/broadcastexceptions.html。

image-20220906125710676

image-20220906125817058

Exported 属性表示是否允许这个 BroadcastReceiver 接收本程序以外的广播,Enabled 属性表示是否启用这个 BroadcastReceiver。

静态的BroadcastReceiver一定要在AndroidManifest.xml文件中注册才可以使用。如果是使用Android Studio的快捷方式创建的BroadcastReceiver,因此注册这一步已经自动完成了。

image-20220722203006676

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.lmc.android28" >
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Android28"
tools:targetApi="31" >
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>

<activity
android:name=".MainActivity"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

由于Android系统启动完成后会发出一条值为android.intent.action.BOOT_COMPLETED 的广播,因此我们在标签中又添加了一个标签,并在里面声明了相应的action。

另外,这里有非常重要的一点需要说明。Android 系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,必须在 AndroidManifest.xml 文件中进行权限声明,否则程序将会直接崩溃。比如这里接收系统的开机广播就是需要进行权限声明的,所以我们在上述代码中使用 标签声明android.permission.RECEIVE_BOOT_COMPLETED 权限。

到目前为止,我们在BroadcastReceiver的onReceive()方法中只是简单地使用Toast提示了一段文本信息,当你真正在项目中使用它的时候,可以在里面编写自己的逻辑。需要注意的是,不要在onReceive()方法中添加过多的逻辑或者进行任何的耗时操作,因为 BroadcastReceiver 中是不允许开启线程的,当 onReceive()方法运行了较长时间而没有结束时,程序就会出现错误。

image-20220906142817709

6.3 发送自定义广播

6.3.1 发送标准广播

image-20220722203607436

1
2
3
4
5
6
7
8
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.lmc.android28.MY_BROADCAST"/>
</intent-filter>
</receiver>
1
2
3
4
5
button.setOnClickListener {
val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
intent.setPackage(packageName)
sendBroadcast(intent)
}

首先构建了一个 Intent 对象,并把要发送的广播的值传入。然后调用 Intent 的setPackage() 方法,并传入当前应用程序的包名。packageName 是 getPackageName() 的语法糖写法,用于获取当前应用程序的包名。最后调用 sendBroadcast() 方法将广播发送出去,这样所有监听 com.example.broadcasttest.MY_BROADCAST 这条广播的 BroadcastReceiver 就会收到消息了。此时发出去的广播就是一条标准广播。

在Android8.0系统之后,静态注册的BroadcastReceiver是无法接收隐式广播的,而默认情况下我们发出的自定义广播恰恰都是隐式广播。因此这里一定要调用setPackage()方法,指定这条广播是发送给哪个应用程序的,从而让它变成一条显式广播,否则静态注册的BroadcastReceiver将无法接收到这条广播。

image-20220906145122440

可以在Intent中携带一些数据传递给相应的
BroadcastReceiver,这一点和Activity的用法是比较相似

6.3.2 发送有序广播

1
2
3
4
5
button.setOnClickListener {
val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
intent.setPackage(packageName)
sendOrderedBroadcast(intent, null)
}

可以看到,发送有序广播只需要改动一行代码,即将 sendBroadcast() 方法改成
sendOrderedBroadcast() 方法。sendOrderedBroadcast() 方法接收两个参数:第一个参数仍然是 Intent;第二个参数是一个与权限相关的字符串,这里传入 null 就行了。

如何设定BroadcastReceiver的先后顺序呢?

1
2
3
<intent-filter android:priority="100">
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>

我们通过android:priority属性给BroadcastReceiver设置了优先级,优先级比较高的BroadcastReceiver就可以先收到广播。 如果在onReceive()方法中调用 abortBroadcast()方法,就表示将这条广播截断,后面的 BroadcastReceiver 将无法再接收到这条广播。

在MyBroadcastReceiver.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.lmc.android28

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class MyBroadcastReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {
// This method is called when the BroadcastReceiver is receiving an Intent broadcast.
Toast.makeText(context,"received in MyBroadcastReceiver",Toast.LENGTH_SHORT).show()
abortBroadcast()//截断广播的传播
}
}

image-20220906151444360

6.4 广播实践:强制下线

项目描述:

无论在哪一个界面,界面上弹出来一个对话框,中间用户无法操作,必须点击确认然后去重新登录

6.4.1 准备

  1. ActivityCollector和BaseActivity

6.4.2 登陆界面处理

  1. 一个LoginActivity和他的布局文件

    xml:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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"
    android:orientation="vertical"
    tools:context=".LoginActivity">
    <LinearLayout
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="60dp">
    <TextView
    android:layout_width="90dp"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:textSize="18sp"
    android:text="Account:"/>
    <EditText
    android:id="@+id/accountEdit"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:layout_gravity="center_vertical"/>

    </LinearLayout>
    <LinearLayout
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="60dp">
    <TextView
    android:layout_width="90dp"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:textSize="18sp"
    android:text="Password:"/>
    <EditText
    android:id="@+id/passwordEdit"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:layout_gravity="center_vertical"
    android:inputType="textPassword"
    />

    </LinearLayout>
    <Button
    android:id="@+id/login"
    android:layout_width="200dp"
    android:layout_height="60dp"
    android:layout_gravity="center_horizontal"
    android:text="Login"/>
    </LinearLayout>

    LoginActivity:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.lmc.broastcastbestpractice

    import android.content.Intent
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.widget.Button
    import android.widget.EditText
    import android.widget.Toast

    class LoginActivity : BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_login)
    findViewById<Button>(R.id.login).setOnClickListener {
    val account = findViewById<EditText>(R.id.accountEdit).text.toString()
    val password = findViewById<EditText>(R.id.passwordEdit).text.toString()
    if (account == "admin" && password == "123456"){
    val intent = Intent(this, MainActivity::class.java)
    startActivity(intent)
    finish()
    }else{
    Toast.makeText(this,"account or password is invalid",Toast.LENGTH_SHORT).show()
    }

    }
    }
    }

6.4.3 MainActivity

xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".MainActivity">

<Button
android:id="@+id/forceOffline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send force offline broadcast"/>

</LinearLayout>

kt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.lmc.broastcastbestpractice

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button

class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.forceOffline).setOnClickListener {
val intent = Intent("com.lmc.broastcastbestpractice.FORCE_OFFLINE")
sendBroadcast(intent)
}
}
}

强制用户下线的逻辑并不是写在MainActivity里的,而是应该写
在接收这条广播的BroadcastReceiver里。这样强制下线的功能就不会依附于任何界面了,不
管是在程序的任何地方,只要发出这样一条广播,就可以完成强制下线的操作了。

6.4.4 接受广播

怎末解决接受广播的,在任何一个地方都可以弹出来一个控件堵塞用户操作?

接下来我们就需要创建一个BroadcastReceiver来接收这条强制下线广播。唯
一的问题就是,应该在哪里创建呢?由于BroadcastReceiver中需要弹出一个对话框来阻塞用
户的正常操作,但如果创建的是一个静态注册的BroadcastReceiver,是没有办法在
onReceive()方法里弹出对话框这样的UI控件的,而我们显然也不可能在每个Activity中都注
册一个动态的BroadcastReceiver。
那么到底应该怎么办呢?答案其实很明显,只需要在BaseActivity中动态注册一个
BroadcastReceiver就可以了,因为所有的Activity都继承自BaseActivity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.lmc.broastcastbestpractice

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity

open class BaseActivity : AppCompatActivity() {
lateinit var receiver: ForceOfflineReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("BaseActivity", javaClass.simpleName)
ActivityCollector.addActivity(this)
}

override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivity(this)
}
//onResume()和onPause()/在这两个方法里注册和取消注册了ForceOfflineReceiver。
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter()
intentFilter.addAction("com.lmc.broastcastbestpractice.FORCE_OFFLINE")
receiver = ForceOfflineReceiver()
registerReceiver(receiver, intentFilter)
}

override fun onPause() {
super.onPause()
unregisterReceiver(receiver)
}
//负责接受强制下线的广播
inner class ForceOfflineReceiver : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
if (p0 != null) {
AlertDialog.Builder(p0).apply {
setTitle("Warning")
setMessage("You are forced to be offline.please try to login again.")
setCancelable(false)
setPositiveButton("OK") { _, _ ->
ActivityCollector.finishAll()//销毁所有的Activity
val i = Intent(context, LoginActivity::class.java)
context.startActivity(i)
}
show()
}
}
}

}
}

为什么要这样写呢?之前不都是在onCreate()和onDestroy()方法里注册和取消注册
BroadcastReceiver的吗?这是因为我们始终需要保证只有处于栈顶的Activity才能接收到这
条强制下线广播,非栈顶的Activity不应该也没必要接收这条广播,所以写在onResume()和
onPause()方法里就可以很好地解决这个问题,当一个Activity失去栈顶位置时就会自动取消
BroadcastReceiver的注册。

6.4.5 AndroidManifest

主Activity改为LoginActivity

七,数据存储方案/持久化技术

7.1 持久化技术简介

数据持久化:将那些内存中的瞬时数据保存到存储设备中

Android系统中主要提供了3种方式用于简单地实现数据持久化功能:

  1. 文件存储

  2. SharedPreferences存储

  3. 数据库存储。

7.2 文件存储

不对存储的内容进行任何格式化处理,数据原封不动地保存到文件中/

适合:存储一些简单的文本数据或二进制数据。

不适合:较为复杂的结构化数据

7.2.1 将数据存储到文件中

Context类中的openFileOutput()方法

【接收参数】:

  1. 文件名(所有的文件都默认存储到/data/data//files/目录下)

  2. 文件的操作模式

    MODE_PRIVATE(默认)同名覆盖

    MODE_APPEND 同名追加

openFileOutput()方法返回的是一个 FileOutputStream 对象,之后用 Java 流的方式将数据写入文件。

1
2
3
4
5
6
7
8
9
10
11
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()
}
}

use函数:Kotlin提供的一个内置扩展函数。保证在 Lambda 表达式中的代码全部执行完之后自动将外层的流关闭

Kotlin是没有异常检查机制(checked exception)的。这意味着使用Kotlin编写的所有代码都不会强制要求你进行异常捕获或异常抛出。上述代码中的try catch代码块是参照 Java 的编程规范添加的,即使你不写try catch代码块,在Kotlin中依然可以编译通过。

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
xml<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"
/>
</LinearLayout>

在 MainActivity 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onDestroy() {
super.onDestroy()
val inputText = editText.text.toString()
save(inputText)
}
private 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()
}
}
}

查看:

Device File Explorer

Ctrl + Shift + A

地址:/data/data/com.example.filepersistencetest/files/

7.2.2 从文件中读取数据

Context类中还提供了一个openFileInput()方法

接收一个参数:文件名

系统会自动到/data/data//files/目录下加载这个文件,并返回一个FileInputStream对象,得到这个对象之后,再通过流的方式就可以将数据读取出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun load(): String {
val content = StringBuilder()www.blogss.cn
try {
val input = openFileInput("data")
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
content.append(it)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
return content.toString()
}

forEachLine函数

将读到的每行内容都回调到Lambda表达式中,我们在 Lambda 表达式中完成拼接逻辑即可

改一下 MainActivity 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inputText = load()
if (inputText.isNotEmpty()) {
editText.setText(inputText)
editText.setSelection(inputText.length)
Toast.makeText(this, "Restoring succeeded", Toast.LENGTH_SHORT).show()
}
}
private fun load(): String {
val content = StringBuilder()
try {
val input = openFileInput("data")
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
content.append(it)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
return content.toString()
}
...
}

setSelection()方法将输入光标移动到文本的末尾位置以便继续输入

7.3 SharedPreferences存储

键值对的方式来存储数据

支持数据类型存储

7.3.1 将数据存储到SharedPreferences中

要想使用SharedPreferences存储数据,首先需要获取SharedPreferences对象。

Android 中主要提供了以下两种方法用于得到 SharedPreferences 对象。

获取 SharedPreferences
  1. Context类中的getSharedPreferences()方法

    接收参数:

    文件的名称

    (SharedPreferences文件都是存放在/data/data//shared_prefs/目录下的)

    操作模式 默认的MODE_PRIVATE

    它和直接传入0的效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。

  2. Activity类中的getPreferences()方法

    操作模式参数

    因为使用这个方法时会自动将当前Activity的类名作为 SharedPreferences的文件名。


向SharedPreferences文件中存储数据步骤:
(1) 调用SharedPreferences对象的edit()方法获取SharedPreferences.Editor对象。
(2) 向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法,添加一个字符串则使用putString()方法,以此类推。
(3) 调用apply()方法将添加的数据提交,从而完成数据存储操作。

xml:

1
2
3
4
5
6
7
8
9
10
11
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/saveButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Data"
/>
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
saveButton.setOnClickListener {
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()
}
}
}

SharedPreferences文件是使用XML格式来对数据进行管理的。

7.3.2 从SharedPreferences中读取数据

这些get方法都接收两个参数:

第一个参数是键,传入存储数据时使用的键就可以得到相应的值了;

第二个参数是默认值,即表示当传入的键找不到对应的值时会以什么样的默认值进行返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/saveButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save Data"
/>
<Button
android:id="@+id/restoreButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Restore Data"
/>
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
restoreButton.setOnClickListener {
val prefs = getSharedPreferences("data", Context.MODE_PRIVATE)
val name = prefs.getString("name", "")
val age = prefs.getInt("age", 0)
val married = prefs.getBoolean("married", false)
Log.d("MainActivity", "name is $name")
Log.d("MainActivity", "age is $age")
Log.d("MainActivity", "married is $married")
}
}
}

首先通过getSharedPreferences()方法得到了SharedPreferences对象,然后分别调用它的getString()、getInt()和getBoolean()方法,去获取前面所存储的姓名、年龄和是否已婚,如果没有找到相应的值,就会使用方法中传入的默认值来代替,最后通过Log将这些值打印出来。

7.3.3 实现记住密码功能

在上一个章节的实践中有一个登陆界面

activity_login.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/rememberPass"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="Remember password" />
</LinearLayout>
<Button
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Login" />
</LinearLayout>

LoginActivity中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
val prefs = getPreferences(Context.MODE_PRIVATE)www.blogss.cn
val isRemember = prefs.getBoolean("remember_password", false)
if (isRemember) {
// 将账号和密码都设置到文本框中
val account = prefs.getString("account", "")
val password = prefs.getString("password", "")
accountEdit.setText(account)
passwordEdit.setText(password)
rememberPass.isChecked = true
}
login.setOnClickListener {
val account = accountEdit.text.toString()
val password = passwordEdit.text.toString()
// 如果账号是admin且密码是123456,就认为登录成功
if (account == "admin" && password == "123456") {
val editor = prefs.edit()
if (rememberPass.isChecked) { // 检查复选框是否被选中
editor.putBoolean("remember_password", true)
editor.putString("account", account)
editor.putString("password", password)
} else {
editor.clear()
}
editor.apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(this, "account or password is invalid",
Toast.LENGTH_SHORT).show()
}
}
}
}

CheckBox。这是一个复选框控件,用户可以通过点击的方式进行选中和取消,我们就使用这个控件来表示用户是否需要记住密码。

7.4 SQLite数据库

文件存储和SharedPreferences存储:保存一些简单的数据和键值对,

SQLite:大量复杂的关系型数据

SQLiteOpenHelper帮助类(抽象类) 适用于创建和升级数据库

7.4.1 创建数据库

SQLiteOpenHelper是 抽象类 -> 继承

必须实现的两抽象方法:onCreate()和 onUpgrade() 创建和升级数据库的逻辑。

构造方法,函数较少的构造方法中接收4个参数:

第一个参数:Context:上下文

第二个参数:数据库名

第三个参数:在查询数据的时候返回一个自定义的Cursor,一般写null

第四个参数:当前数据库版本号,用于数据库升级

两实例方法:getReadableDatabase()和 getWritableDatabase()。创建/打开一个现有的数据库(数据库存在->打开,否则->创建),返回一个对数据库进行读写操作的对象。

区别:当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式打开数据库,而 getWritableDatabase() 方法会出现异常。

创建数据库步骤:

  1. 构建SQLiteOpenHelper的实例

  2. 调用它的getReadableDatabase()或getWritableDatabase()方法了

当数据库创建的时候,重写的onCreate()方法也会得到执行,所以通常会在这里处理一些创建表的逻辑。

(数据库文件会存放在/data/data//databases/目录下)

建立一个 BookStore.db 然后一张 Book 表

1
2
3
4
5
6
create table Book (
id integer primary key autoincrement,
author text,
price real,
pages integer,
name text)

integer表示整型,real表示
浮点型,text表示文本类型,blob表示二进制类型

MyDatabaseHelper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.lmc.android34

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.widget.Toast

class MyDatabaseHelper(val context: Context, name: String, version: Int) :
SQLiteOpenHelper(context, name, null, version) {
//一 SQL 语句区
private val createBook = "create table Book ( " +
"id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)"

//二,两个抽象方法
override fun onCreate(p0: SQLiteDatabase?) {
//这里p0做了一点处理,和《第一行代码》有些许区别,注意别写错了
p0?.apply {
execSQL(createBook)
Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
}
}

override fun onUpgrade(p0: SQLiteDatabase?, p1: Int, p2: Int) {}
}

MainActivity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.lmc.android34

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

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

//1. 拿到 SQLiteOpenHelper 实例对象
val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)
findViewById<Button>(R.id.createDatabase).setOnClickListener {
//2.创建/打开数据库
dbHelper.writableDatabase
}
}
}

验证:

Device File Explorer (Save As) + Database Navigator 插件 【细节注意,玄学Bug】

7.4.2 升级数据库

一般都是再加一张表

在 MyDatabaseHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.lmc.android34

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.widget.Toast

class MyDatabaseHelper(val context: Context, name: String, version: Int) :
SQLiteOpenHelper(context, name, null, version) {

//一 SQL 语句区
private val createBook = "create table Book ( " +
"id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)"
//升级1
private val createCategory = "create table Category (" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"

//二,两个抽象方法
override fun onCreate(p0: SQLiteDatabase?) {
p0?.apply {//现在这个优势就体现出来了
execSQL(createBook)
//升级2
execSQL(createCategory)
Toast.makeText(context, "Create succeeded", Toast.LENGTH_SHORT).show()
}
}

override fun onUpgrade(p0: SQLiteDatabase?, p1: Int, p2: Int) {
//升级3
p0?.apply {
execSQL("drop table if exists Book")
execSQL("drop table if exists Category")
onCreate(p0)
}
}

}

记得在 MainActivity 中那个版本号要改的比之前那个要大

7.4.3 添加数据

SQLiteOpenHelper的getReadableDatabase() 或 getWritableDatabase() 方法会返回一个 SQLiteDatabase 对象,借助这个对象就可以对数据进行 CRUD 操作了。

SQLiteDatabase 提供了一个 insert() 方法,接收 3 个参数:

第一个参数:表名;

第二个参数:用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般用不到这直接传入null;

第三个参数:一个 ContentValues对象,提供了一系列的 put() 方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。

xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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/addData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add Data"/>

</LinearLayout>

kt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)

...
addData.setOnClickListener {
val db = dbHelper.writableDatabase
val values1 = ContentValues().apply {
// 开始组装第一条数据
put("name", "西游记")
put("author", "吴承恩")
put("pages", 454)
put("price", 16.96)
}
db.insert("Book", null, values1) // 插入第一条数据
val values2 = ContentValues().apply {
// 开始组装第二条数据
put("name", "兄弟")
put("author", "余华")
put("pages", 510)
put("price", 19.95)
}
db.insert("Book", null, values2) // 插入第二条数据
}
}
}

7.4.4 更新数据

SQLiteDatabase中的update()方法。

接收4个参数:

第一个参数:表名;

第二个参数:ContentValues对象,要把更新数据在这里组装进去;

第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认会更新所有行。

xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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/updateData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update Data"
/>
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
...
updateData.setOnClickListener {
val db = dbHelper.writableDatabase
val values = ContentValues()
values.put("price", 9999)
db.update("Book", values, "name = ?", arrayOf("西游记"))
}
}
}

这里使用了第三、第四个参数来指定具体更新哪几行。第三个参数对应的是SQL语句的 where 部分,表示更新所有name等于?的行,而?是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容,arrayOf() 方法是 Kotlin 提供的一种用于便捷创建数组的内置方法。

7.4.5 删除数据

SQLiteDatabase中提供delete()方法

3个参数:表名;第二、第三个参数用于约束删除某一行或某几行的数据,不指定的话默认会删除所有行。

1
2
3
4
5
6
7
8
9
10
11
12
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
...
deleteData.setOnClickListener {
val db = dbHelper.writableDatabase
db.delete("Book", "pages > ?", arrayOf("500"))
}
}
}

7.4.6 查询数据

QLiteDatabase中提供了query()方法用于对数据查询。

这个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。

7个参数:

第一个参数:表名。

第二个参数:用于指定去查询哪几列,如果不指定则默认查询所有列。

第三、第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据。

第五个参数用于指定需要去group by的列,不指定则表示不对查询结果进行group by操作。

第六个参数用于对group by之后的数据进行进一步的过滤,不指定则表示不进行过滤。

第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。

调用query()方法后会返回一个Cursor对象,查询到的所有数据都将从这个对象中取出。

image-20220723195224048

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
...
queryData.setOnClickListener {
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()
}
}
}

7.4.7 使用SQL操作数据库

image-20220723195731858

7.5 SQLite数据库的最佳实践

事务的特性可以保证让一系列的操作要么全部完成,
要么一个都不会完成。

7.5.1 使用事务

1
2
3
4
5
6
7
8
9
10
11
12
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
...
<Button
android:id="@+id/replaceData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Replace Data"
/>
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this, "BookStore.db", 2)
...
replaceData.setOnClickListener {
val db = dbHelper.writableDatabase
db.beginTransaction() // 开启事务
try {
db.delete("Book", null, null)
if (true) {
// 手动抛出一个异常,让事务失败
throw NullPointerException()
}
val values = ContentValues().apply {
put("name", "Game of Thrones")
put("author", "George Martin")
put("pages", 720)
put("price", 20.85)
}
db.insert("Book", null, values)
db.setTransactionSuccessful() // 事务已经执行成功
} catch (e: Exception) {
e.printStackTrace()
} finally {
db.endTransaction() // 结束事务
}
}
}
}

7.5.2 升级数据库的最佳写法

这里需要为每一个版本号赋予其所对应的数据库变动,然后在onUpgrade()方法
中对当前数据库的版本号进行判断,再执行相应的改变就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)"
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)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion <= 1) {
db.execSQL(createCategory)
}
}
}

新的需求:需要在Book 表中添加一个category_id字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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," +
//这里加一个
"category_id integer)"
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)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion <= 1) {
db.execSQL(createCategory)
}
//这里再加一句
if (oldVersion <= 2) {
db.execSQL("alter table Book add column category_id integer")
}
}
}

为什么设置多层if< 判断?

服务App跨版本升级

八,ContentProvider

Android中不同APP之间数据共享的一种手段

8.1 运行时权限

Android目前权限分为两类

普通权限:在AndroidManifest.xml文件中添加权限声明后就可以直接使用(系统自动)

危险权限:特殊对待运行时权限处理

到 Android 10 系统为止所有的危险权限

image-20220724161210148

用户一旦同意了某个权限申请之后,同组的其他权限也会被系统自动授权。

但Android系统随时有可能调整权限的分组。 ,利用这个小Bug貌似没啥用~

8.2 在程序运行时申请权限的演示

Android 6.0开始在使用危险权限时必须进行运行时权限处理

新方法简介:

ContextCompat.checkSelfPermission() 判断是否拿到某权限

接收两个参数:第一个Context,第二个具体的权限名。比如Manifest.permission.CALL_PHONE(打电话)

ActivityCompat.requestPermissions() 向用户申请授权。
接收3个参数:第一个参数要求是Activity的实例(在哪一个Activity中申请权限);第二个参数是一个String数组,里面是申请的权限名;第三个参数是请求码,要求是唯一值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class MainActivity : AppCompatActivity() {

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

findViewById<Button>(R.id.makeCall).setOnClickListener {
//判断是否获取目标权限
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
//没有就申请权限
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.CALL_PHONE), 1)
} else {
//有就直接执行
call()
}
}

}

//处理请求权限结果
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1 -> {
if (grantResults.isNotEmpty()
&& grantResults[0] == PackageManager.PERMISSION_GRANTED){
call()
} else {
Toast.makeText(this, "You denied the permission",
Toast.LENGTH_SHORT).show()
}
}
}
}

//把打电话的逻辑封装成一个方法
private fun call() {
try {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
} catch (e: SecurityException) {
e.printStackTrace()
}
}
}

运行时异常给了一次就默认一直有了,要取消去设置看

这里当初出了一个玄学Bug,苦寻无果。最后是过了很久换了一个项目才跑起来的。

Screenshot_20220923_103949_com.android.permission

Screenshot_20220923_200033_com.android.incallui

8.3 访问其他程序中的数据

ContentProvider 就像APP上一个对外接口,其他APP可以通过这个口子去访问他的数据

8.3.1 访问ContentProvider

ContentResolver类负责这方面。

获取该类的实例:可以通过Context中的getContentResolver()方法。

提供对应增删改查操作的数据处理方法 insert() update() delete() query()

不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。

内容URI给ContentProvider中的数据建立了唯一标识符,它主要由三部分组成:协议申明+authority+path。

协议声明:统一是 content://
authority 区分不同APP,一般采用包名
path 区分同一APP下的不同表名,一般直接表名就可以了
内容URI字符串举例:

content://com.example.app.provider/table1
content://com.example.app.provider/table2

val uri = Uri.parse(“content://com.example.app.provider/table1”)
只需要调用Uri.parse()方法,就可以将内容URI字符串解析成Uri对象,Uri对象才能当参数传递

query方法详解

image-20220923210712338

具体代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//query
val cursor = contentResolver.query(
uri,
projection,
selection,
selectionArgs,
sortOrder)
while (cursor.moveToNext()) {
val column1 = cursor.getString(cursor.getColumnIndex("column1"))
val column2 = cursor.getInt(cursor.getColumnIndex("column2"))
}
cursor.close()
//add
val values1 = contentValuesOf("column1" to "text", "column2" to 1)
contentResolver.insert(uri, values1)
//update
val values2 = contentValuesOf("column1" to "")
contentResolver.update(uri, values2, "column1 = ? and column2 = ?", arrayOf("text", "1"))
//delete
contentResolver.delete(uri, "column2 = ?", arrayOf("1"))
8.3.2 读取系统联系人

xml文件

1
2
3
4
5
6
7
8
9
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ListView
android:id="@+id/contactsView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

kt文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package com.lmc.android28

import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.provider.ContactsContract
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat

class MainActivity : AppCompatActivity() {

private val contactsList = ArrayList<String>()//联系人信息
private lateinit var adapter: ArrayAdapter<String>//ListView适配器

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

//给ListView控件做Adapter适配器
adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, contactsList)
val contactsView = findViewById<ListView>(R.id.contactsView)
contactsView.adapter = adapter
//检查是否授权
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_CONTACTS
) != PackageManager.PERMISSION_GRANTED
) {
//没有授权就申请个(运行时申请)
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS), 1)
} else {
//有了就直接开始读取通讯录联系人
readContacts()
}
}

//对运行时申请权限结果的处理
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)

when (requestCode) {
1 -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
readContacts()
} else {
//弹出申请权限被拒绝的提示
Toast.makeText(this, "您申请的权限被拒绝~", Toast.LENGTH_SHORT).show()
}
}
}
}

//读取联系人的方法
@SuppressLint("Range")
private fun readContacts() {
//查询联系人数据
contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,//这是 ContactsContract.CommonDataKinds.Phone 类已经帮我们做好了封装,提供了一个 CONTENT_URI 常量,而这个常量就是使用 Uri.parse() 方法解析出来的结果。
null,
null,
null,
null
)?.apply {
while (moveToNext()) {
//姓名
val displayName = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
//手机号
val number = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))

contactsList.add("$displayName\n$number")
}
adapter.notifyDataSetInvalidated()
close()
}
}
}
1
2
3
4
5
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.contactstest">
<uses-permission android:name="android.permission.READ_CONTACTS" />
...
</manifest>

貌似我的包名当初写错啦?

Screenshot_20220923_225307_com.android.permission

AAA

8.4 创建自己的 ContentProvider

8.4.1 理论知识

想要自己的APP的数据也可以让别的APP共享,可以新建一个类去继承 ContentProvider ,然后实现他的6个抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class MyProvider : ContentProvider() {

/**
*(1) onCreate()。初始化ContentProvider的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true表示ContentProvider初始化成功,返回false 则表示失败。
*/
override fun onCreate(): Boolean {
return false
}

/**
*(2) query()。从ContentProvider中查询数据。uri参数用于确定查询哪张表,projection参数用于确定查询哪些列,selection和selectionArgs参数用于约束查 询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
*/
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
return null
}

/**
(3) insert()。向ContentProvider中添加一条数据。uri参数用于确定要添加到的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新记录 的URI。
*/
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}

/**
*(4) update()。更新ContentProvider中已有的数据。uri参数用于确定更新哪一张表中的数据,新数据保存在values参数中,selection和selectionArgs参数用于 约束更新哪些行,受影响的行数将作为返回值返回。
*/
override fun update(uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?): Int {
return 0
}

/**
*(5) delete()。从ContentProvider中删除数据。uri参数用于确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将 作为返回值返回。
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}

/**
*(6) getType()。根据传入的内容URI返回相应的MIME类型。
*/
override fun getType(uri: Uri): String? {
return null
}

}

URI

标准:content://com.example.app.provider/table1

这就表示调用方期望访问的是com.example.app这个应用的table1表中的数据。

精准:content://com.example.app.provider/table1/1

这就表示调用方期望访问的是com.example.app这个应用的table1表中id为1的数据

image-20220724210009656

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class MyProvider : ContentProvider() {
private val table1Dir = 0
private val table1Item = 1
private val table2Dir = 2
private val table2Item = 3
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
/**
*UriMatcher中
提供了一个addURI()方法,这个方法接收3个参数,可以分别把authority、path和一个自
定义代码传进去。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传www.blogss.cn
入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,
*/
uriMatcher.addURI("com.example.app.provider", "table1", table1Dir)
uriMatcher.addURI("com.example.app.provider ", "table1/#", table1Item)
uriMatcher.addURI("com.example.app.provider ", "table2", table2Dir)
uriMatcher.addURI("com.example.app.provider ", "table2/#", table2Item)
}
...
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
//知道你想访问的是哪一个操作
when (uriMatcher.match(uri)) {
table1Dir -> {
// 查询table1表中的所有数据
}
table1Item -> {
// 查询table1表中的单条数据
}
table2Dir -> {
// 查询table2表中的所有数据
}
table2Item -> {
// 查询table2表中的单条数据
}
}
...
}
...
}

getType() 方法

它是所有的ContentProvider都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容URI 更多操作所对应的MIME字符串主要由部分组成,格式规定。

  • 必须以vnd开头。
  • 如果内容URI以路径结尾,则后接android.cursor.dir/;如果内容URI以id结尾,则后
    接android.cursor.item/。
  • 最后接上vnd..
内容URI MUME
content://com.example.app.provider/table1 vnd.android.cursor.dir/vnd.com.example.app.provider.table1
content://com.example.app.provider/table1/1 vnd.android.cursor.item/vnd.com.example.app.provider.table1
1
2
3
4
5
6
7
8
9
10
class MyProvider : ContentProvider() {
...
override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
table1Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
table1Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table1"
table2Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"
table2Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table2"
else -> null
}
}

为什么 ContentResolver能保证隐私数据不会泄漏出去呢?
因为所有的增删改查操作都一定要匹配到相应的内容URI格式才能进行,而我们当然不可能向UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问,安全问题也就不存在了。

8.4.2 代码实践

为了方便我们在Android34 来新建ContentProvider,AS会自动做其他工作,很方便的。

image-20220924121419059

image-20220924121648990

DatebaseProvider

跨程序访问时我们不能直接使用Toast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package com.lmc.android34

import android.content.ContentProvider
import android.content.ContentValues
import android.content.UriMatcher
import android.net.Uri

class DatabaseProvider : ContentProvider() {

private val bookDir = 0
private val bookItem = 1
private val categoryDir = 2
private val categoryItem = 3
private val authority = "com.lmc.android34.provider"
private lateinit var dbHelper: MyDatabaseHelper//这里只是拿到了SQLite类的实例

//替UriMatcher做的准备
private val uriMatcher by lazy {
val matcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(authority, "book", bookDir)
addURI(authority, "book/#", bookItem)
addURI(authority, "category", categoryDir)
addURI(authority, "category/#", categoryItem)
}
matcher
}
/*
*只有by才是Kotlin中的关键字,lazy在这里只是一个高阶函数而已。在lazy函数中会创建
并返回一个Delegate对象,当我们调用p属性的时候,其实调用的是Delegate对象的
getValue()方法,然后getValue()方法中又会调用lazy函数传入的Lambda表达式,这样
表达式中的代码就可以得到执行了,并且调用p属性后得到的值就是Lambda表达式中最后一行
代码的返回值。
*/

override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) =
dbHelper.let {
//删除数据
val db = it.writableDatabase//拿到操作数据库的对象
val deleteRow = when (uriMatcher.match(uri)) {
bookDir -> db.delete("Book", selection, selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]//【1】是id,【0】是路径
db.delete("Book", "id = ?", arrayOf(bookId))
}
categoryDir -> db.delete("Category", selection, selectionArgs)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.delete("Category", "id = ?", arrayOf(categoryId))
}
else -> 0
}
deleteRow
}

override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
bookDir -> "vnd.android.cursor.dir/vnd.com.lmc.android34.provider.book"
bookItem -> "vnd.android.cursor.item/vnd.com.lmc.android34.provider.book"
categoryDir -> "vnd.android.cursor.dir/vnd.com.lmc.android34.provider.category"
categoryItem -> "vnd.android.cursor.item/vnd.com.lmc.android34.provider.category"
else -> null
}

override fun insert(uri: Uri, values: ContentValues?) = dbHelper.let {
// 添加数据
val db = it.writableDatabase
val uriReturn = when (uriMatcher.match(uri)) {
bookDir, bookItem -> {
val newBookId = db.insert("Book", null, values)//新插入行的行Id,异常就是-1
Uri.parse("content://$authority/book/$newBookId")
}
categoryDir, categoryItem -> {
val newCategoryId = db.insert("Category", null, values)
Uri.parse("content://$authority/category/$newCategoryId")
}
else -> null
}
uriReturn//返回被修改数据的URI信息
}

//Getter语法糖,?.操作符,let函数,?:操作符,单行代码函数语法糖
override fun onCreate() = context?.let {
dbHelper = MyDatabaseHelper(it, "BookStore.db", 2)
true
} ?: false

override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
) = dbHelper.let {
//查询数据
val db = it.readableDatabase//我们这里是查询所以用readable就可以了
val cursor = when (uriMatcher.match(uri)) {
bookDir -> db.query(
"Book",
projection,
selection,
selectionArgs,
null,
null,
sortOrder)
bookItem -> {
/**
*将内容URI权限之后的部分以“/”符号进行分割,并把分割后
*的结果放入一个字符串列表中,那这个列表的第0个位置存放的就是路径,第1个位置存放的就是id
*/
val bookId = uri.pathSegments[1]
db.query(
"Book",
projection,
"id = ?",
arrayOf(bookId),
null,
null,
sortOrder)
}
categoryDir -> db.query(
"Category",
projection,
selection,
selectionArgs,
null,
null,
sortOrder
)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.query(
"Category",
projection,
"id = ?",
arrayOf(categoryId),
null,
null,
sortOrder
)
}
else -> null
}
cursor
}

override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
) = dbHelper.let {
// 更新数据
val db = it.writableDatabase
val updatedRows = when (uriMatcher.match(uri)) {
bookDir -> db.update("Book", values, selection, selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]
db.update("Book", values, "id = ?", arrayOf(bookId))
}
categoryDir -> db.update("Category", values, selection, selectionArgs)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.update("Category", values, "id = ?", arrayOf(categoryId))
}
else -> 0
}
updatedRows
}
}

下面是测试案例

首先,把虚拟器上的那个APP卸载重装

然后新建一个项目

xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".MainActivity">

<Button
android:id="@+id/addData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add To Book" />
<Button
android:id="@+id/queryData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Query From Book" />
<Button
android:id="@+id/updateData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update Book" />
<Button
android:id="@+id/deleteData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete From Book" />

</LinearLayout>

kt文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.lmc.android39

import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.core.content.contentValuesOf

class MainActivity : AppCompatActivity() {
var bookId: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

findViewById<Button>(R.id.addData).setOnClickListener {
// 添加数据
val uri = Uri.parse("content://com.lmc.android34.provider/book")
val values = contentValuesOf("name" to "新神传 杨戬",
"author" to "猫眼娱乐", "pages" to 140, "price" to 22.8)
val newUri = contentResolver.insert(uri, values)
bookId = newUri?.pathSegments?.get(1)
}
findViewById<Button>(R.id.deleteData).setOnClickListener {
// 删除数据
bookId?.let {
val uri = Uri.parse("content://com.lmc.android34.provider/book/$it")
contentResolver.delete(uri, null, null)
}
}
findViewById<Button>(R.id.queryData).setOnClickListener { // 查询数据
val uri = Uri.parse("content://com.lmc.android34.provider/book")
contentResolver.query(uri, null, null, null, null)?.apply {
while (moveToNext()) {
//下面的代码和《第一行代码 第三版》中有出入,请注意
val name = getString(getColumnIndexOrThrow("name"))
val author = getString(getColumnIndexOrThrow("author"))
val pages = getInt(getColumnIndexOrThrow("pages"))
val price = getDouble(getColumnIndexOrThrow("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")
}
close()
} }
findViewById<Button>(R.id.updateData).setOnClickListener { // 更新数据
bookId?.let {
val uri = Uri.parse("content://com.lmc.android34.provider/book/$it")
val values = contentValuesOf("name" to "明日战纪",
"pages" to 999, "price" to 0.01)
contentResolver.update(uri, values, null, null)
} }
}
}

九,多媒体

9.1 通知

9.1.1 通知渠道

什么是通知渠道呢?

根据通知的重要等级分为不同类

  1. NotificationManager 通知管理

    调用 Context 的getSystemService() 方法获取。
    getSystemService() 方法接收一个字符串参数(获取系统的哪个服务)

    1
    2
    val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    ...
  2. NotificationChannel 通知渠道

    调用 NotificationManager 的 createNotificationChannel() 方法完成创建。

    1
    2
    3
    4
    5
    6
    7
    ...
    //由于NotificationChannel 类和 createNotificationChannel() 方法都是 Android 8.0 系统中新增的API所以要进行版本判断才可以
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    //渠道ID(保证全局唯一)、渠道名称以及初始状态下的重要等级(IMPORTANCE_HIGH/IMPORTANCE_DEFAULT/IMPORTANCE_LOW/IMPORTANCE_MIN)
    val channel = NotificationChannel(channelId, channelName, importance)//构建通知渠道
    manager.createNotificationChannel(channel)
    }

9.1.2 通知的基本用法

  1. 创建 Notification 对象

    • 空的

    首先需要使用一个Builder构造器来创建Notification对象,Android版本迭代大问题多,推荐用 AndroidX 库中提供的兼容API。AndroidX库中提供了一个NotificationCompat 类

    1
    val notification = NotificationCompat.Builder(context, channelId).build()

    NotificationCompat.Builde接收两个参数:第一个参数是 context;第二个参数是渠道ID,也就是说在创建通知的时候就要指明他是什么渠道类型的

    • 实际的
    1
    2
    3
    4
    5
    6
    7
    val notification = NotificationCompat.Builder(context, channelId)
    .setContentTitle("This is content title")//标题
    .setContentText("This is content text")//内容
    .setSmallIcon(R.drawable.small_icon)
    .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.large_icon))
    .build()
    manager.notify(1, notification)//让通知显示处理

    小图标只能使用纯alpha图层的图片进行设置

    notify()方法接收两个参数:第一个参数是id,要保证为每个通知指定的id都是不同的;第二个参数则是Notification对象

  2. 全局 MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val manager = getSystemService(Context.NOTIFICATION_SERVICE) as
    NotificationManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    val channel = NotificationChannel("normal", "Normal",NotificationManager.
    IMPORTANCE_DEFAULT)
    manager.createNotificationChannel(channel)
    }
    sendNotice.setOnClickListener {
    val notification = NotificationCompat.Builder(this, "normal")
    .setContentTitle("This is content title")
    .setContentText("This is content text")
    .setSmallIcon(R.drawable.small_icon)
    .setLargeIcon(BitmapFactory.decodeResource(resources,
    R.drawable.large_icon))
    .build()
    manager.notify(1, notification)
    }
    }
    }

    图片可以自己准备也可以用老师的

Screenshot_20220927_145333_com.lmc.android41

Screenshot_20220927_145328_com.lmc.android41


  1. PendingIntent

    通知被点击反应设置

    获取PendingIntent的实例 -> getActivity()方法、getBroadcast()方法,还是getService()方法。
    这几个方法所接收的参数都是相同的:第一个参数Context;第二个参数传入0即可;第三个参数是一个Intent对象;第四个参数用于确 PendingIntent的行为(FLAG_ONE_SHOT/FLAG_NO_CREATE/FLAG_CANCEL_CURRENT/FLAG_UPDATE_CURRENT)通常情况下传入0。

    NotificationCompat.Builder。这个构造器还可以连缀一个 setContentIntent() 方
    法,接收的参数正是一个PendingIntent对象。因此,这里就可以通过PendingIntent构建一个延迟执行的“意图”,当用户点击这条通知时就会执行相应的逻辑。

    这地方要准备另外一个Activity 作为点击通知就跳转的工具Activity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
    sendNotice.setOnClickListener {
    val intent = Intent(this, OtherActivity::class.java)
    val pi = PendingIntent.getActivity(this, 0, intent, 0)
    val notification = NotificationCompat.Builder(this, "normal")
    .setContentTitle("快看俺鸭!")
    .setContentText("哈哈哈,就是打个招呼没准备干哈啦")
    .setSmallIcon(R.drawable.small_icon)
    .setLargeIcon(BitmapFactory.decodeResource(resources,
    R.drawable.large_icon))
    .setContentIntent(pi)
    .build()
    manager.notify(1, notification)
    }
    }
    }
  2. 点了通知后,通知图标没有消失

    一种是在NotificationCompat.Builder中再连缀一个setAutoCancel()方法,
    一种是在被打开页面中显式地调用NotificationManager的cancel()方法将它取消。

    第一种办法:

    1
    2
    3
    4
    val notification = NotificationCompat.Builder(this, "normal")
    ...
    .setAutoCancel(true)
    .build()

    第二种:

    想取消什么通知就传入对应它的 id

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class NotificationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_notification)

    val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    manager.cancel(1)
    }
    }

9.1.3 通知的进阶技巧

9.1.3.1 setStyle
  1. 通知内容文字过长

setStyle代替setContentText

原来

1
2
3
4
5
6
7
8
9
10
11
val notification = NotificationCompat.Builder(this, "normal")
...
.setContentText("西南大学第八次学代会代表推荐:\n" +
// "一.代表名额及构成\n" +
// "按照学院安排,推荐代表候选人的要求如下:19-21级每个班最多可推荐4名同学,22级每个班最多可推荐一名同学,研究生自愿报名人数不限。\n" +
// "鼓励女生和少数民族报名参加,在推荐条件基础相同的情况下,团支部优先推荐少数民族同学。\n" +
// "二、代表应具备的条件\n" +
// "1.思想信念坚定。深入贯彻习近平新时代中国特色社会主义思想,认真学习习近平总书记关于青年工作的重要思想和关于教育的重要论述,争做有理想、敢担当、能吃苦、肯奋斗的新时代好青年。\n" +
// )
...
.build()

升级

1
2
3
4
5
6
7
8
9
10
val notification = NotificationCompat.Builder(this, "normal")
...
.setStyle(NotificationCompat.BigTextStyle().bigText("西南大学第八次学代会代表推荐:\n" +
// "一.代表名额及构成\n" +
// "按照学院安排,推荐代表候选人的要求如下:19-21级每个班最多可推荐4名同学,22级每个班最多可推荐一名同学,研究生自愿报名人数不限。\n" +
// "鼓励女生和少数民族报名参加,在推荐条件基础相同的情况下,团支部优先推荐少数民族同学。\n" +
// "二、代表应具备的条件\n" +
// "1.思想信念坚定。深入贯彻习近平新时代中国特色社会主义思想,认真学习习近平总书记关于青年工作的重要思想和关于教育的重要论述,争做有理想、敢担当、能吃苦、肯奋斗的新时代好青年。\n" +
// ))
.build()
  1. 显示图片
1
2
3
4
val notification = NotificationCompat.Builder(this, "normal")
...
.setStyle(NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(resources, R.drawable.small_icon)))
.build()
9.1.3.2 不同重要等级渠道对通知行为具体影响

通知渠道的重要等级越高,发出的通知就越容易获得用户的注意。
(打开王者,他自己会弹出来最新的活动,那个活动就是等级最高的通知)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
...
//建立一个新的渠道
val channel2 = NotificationChannel("important", "Important",NotificationManager.IMPORTANCE_HIGH)
manager.createNotificationChannel(channel2)
}
//自己在xml文件中加一个按钮啦
findViewById<Button>(R.id.sendImportantNotice).setOnClickListener {
val intent = Intent(this, OtherActivity::class.java)
val pi = PendingIntent.getActivity(this, 0, intent, 0)
val notification = NotificationCompat.Builder(this, "important")
.setContentTitle("我是重要通知")
.setContentText("捣蛋鬼别捣蛋")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
.setAutoCancel(true)//点击通知后,通知自动消失
.setContentIntent(pi)//设置通知点击事件
.build()//创建通知
manager.notify(1, notification)//让通知显示处理
}
}
}

如果没有浮窗或者声音:关于Android通知的浮动通知(横幅)不显示的解决方法_IT冰棍的博客-CSDN博客_android 不显示浮动通知

Screenshot_20220927_162135_com.lmc.android41

1

9.2 摄像头&相册

9.2.1 调用摄像头拍照

新建一个项目

xml 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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/takePhotoBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Take Photo" />

<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
</LinearLayout>

有个 button 和 一个 image 显示拍出来的照片

/我的手机/Android/data//cache

MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.lmc.android42

import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.media.ExifInterface
import android.media.Image
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Button
import android.widget.ImageView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import java.io.File

class MainActivity : AppCompatActivity() {
lateinit var imageUri: Uri
lateinit var outputImage: File

private val activityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
result ->
if (result.resultCode == RESULT_OK ){
//返回处理
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))//解析为Bitmap对象
findViewById<ImageView>(R.id.imageView).setImageBitmap(rotateIfRequired(bitmap))//让照片在页面控件中显示出来
}
}


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

findViewById<Button>(R.id.takePhotoBtn).setOnClickListener {
//做图片的文件
outputImage = File(externalCacheDir, "output_image.jpg")
if (outputImage.exists()){
outputImage.delete()
}
outputImage.createNewFile()
//拿到照片文件的本地真是路径
imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
FileProvider.getUriForFile(this,"com.lmc.android42.fileprovider",outputImage)
}else{
Uri.fromFile(outputImage)
}

//启动相机程序
val intent = Intent("android.media.action.IMAGE_CAPTURE")
intent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri)//指明照片保存地址
activityResultLauncher.launch(intent)
}
}


//下面的可以看不懂没关系,就是如有需要就把图片反转一下
//判断是否需要旋转
private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
val exif = ExifInterface(outputImage.path)
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_NORMAL)
return when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90)
ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180)
ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270)
else -> bitmap
}
}

private fun rotateBitmap(bitmap: Bitmap, degree: Int): Bitmap {
val matrix = Matrix()
matrix.postRotate(degree.toFloat())
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height,matrix, true)
bitmap.recycle() // 将不再需要的Bitmap对象回收
return rotatedBitmap
}
}

AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.cameraalbumtest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.lmc.android42.fileprovider"
<!-上面这个属性必须跟FileProvider.getUriForFile第二个参数一致 ->
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

右击xml目录→New→File,创建一个file_paths.xml文件

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="/" />
</paths>

external-path就是用来指定Uri共享路径的,name属性的值可以随便填,path属性的值表示共享的具体路径。这里使用一个单斜线表示将整个SD卡进行共享,当然你也可以仅共享存放output_image.jpg这张图片的路径。

9.2.2 从相册中选择图片

在上面的APP中直接改好了

1
2
3
4
5
6
7
8
9
10
11
12
<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/fromAlbumBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="From Album" />
...
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class MainActivity : AppCompatActivity() {
...
private val activityResultLauncher2 = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
result ->
if (result.resultCode == RESULT_OK ){
val data = result.data
if ( data != null){
data.data?.let { uri ->
//将选择的图片显示
val bitmap2 = getBitmapFromUri(uri)
findViewById<ImageView>(R.id.imageView).setImageBitmap(bitmap2)//让照片在页面控件中显示出来
}
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
...
findViewById<Button>(R.id.fromAlbumBtn).setOnClickListener {
//打开文件选择器
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
//指定显示图片
intent.type = "image/*"
activityResultLauncher2.launch(intent)
}
}



//将Uri转换成Bitmap对象
private fun getBitmapFromUri(uri: Uri) = contentResolver
.openFileDescriptor(uri, "r")?.use {
BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
}
...
}

思考:如果照片像素很高直接加载到内存会直接崩溃的,一般会压缩之后再导进来的。

最终成品贴一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package com.lmc.android42

import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.media.ExifInterface
import android.media.Image
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Button
import android.widget.ImageView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import java.io.File

class MainActivity : AppCompatActivity() {

private lateinit var imageUri: Uri
private lateinit var outputImage: File

private val activityResultLauncher1 = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
result ->
if (result.resultCode == RESULT_OK ){
//返回处理
val bitmap1 = BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))//解析为Bitmap对象
findViewById<ImageView>(R.id.imageView).setImageBitmap(rotateIfRequired(bitmap1))//让照片在页面控件中显示出来
}
}


private val activityResultLauncher2 = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){
result ->
if (result.resultCode == RESULT_OK ){

val data = result.data
if ( data != null){
data.data?.let { uri ->
//将选择的图片显示
val bitmap2 = getBitmapFromUri(uri)
findViewById<ImageView>(R.id.imageView).setImageBitmap(bitmap2)//让照片在页面控件中显示出来
}
}

}
}

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

findViewById<Button>(R.id.takePhotoBtn).setOnClickListener {
//做图片的文件
outputImage = File(externalCacheDir, "output_image.jpg")
if (outputImage.exists()){
outputImage.delete()
}
outputImage.createNewFile()
//拿到照片文件的本地真是路径
imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
FileProvider.getUriForFile(this,"com.lmc.android42.fileprovider",outputImage)
}else{
Uri.fromFile(outputImage)
}

//启动相机程序
val intent = Intent("android.media.action.IMAGE_CAPTURE")
intent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri)//指明照片保存地址
activityResultLauncher1.launch(intent)
}

findViewById<Button>(R.id.fromAlbumBtn).setOnClickListener {
//打开文件选择器
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
//指定显示图片
intent.type = "image/*"
activityResultLauncher2.launch(intent)

}
}


//下面的可以看不懂没关系,就是如有需要就把图片反转一下
//判断是否需要旋转
private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
val exif = ExifInterface(outputImage.path)
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_NORMAL)
return when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90)
ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180)
ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270)
else -> bitmap
}
}

private fun rotateBitmap(bitmap: Bitmap, degree: Int): Bitmap {
val matrix = Matrix()
matrix.postRotate(degree.toFloat())
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height,matrix, true)
bitmap.recycle() // 将不再需要的Bitmap对象回收
return rotatedBitmap
}

//将Uri转换成Bitmap对象
private fun getBitmapFromUri(uri: Uri) = contentResolver
.openFileDescriptor(uri, "r")?.use {
BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
}
}

动画

1

2

3

4

9.3 播放多媒体文件

9.3.1 播放音频

MediaPlayer 类

image-20220725213752826

MediaPlayer 的工作流程

  1. 创建MediaPlayer 对象
  2. 调 setDataSource()方法设置音频文件的路径
  3. 调用 prepare() 方法使 MediaPlayer 进入准备状态
  4. 接下来调用 start() 方法就可以开始播放音频
  5. 调用pause()方法就会暂停播放
  6. 调用 reset() 方法就会停止播放。

新建一个项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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/play"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Play" />

<Button
android:id="@+id/pause"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pause" />

<Button
android:id="@+id/stop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Stop" />

</LinearLayout>

main 下创建一个assets目录,并在这个目录下存放任意文件和子目录,这些文件和子目录在项目打包时会一并被打包到安装文件中,然后我们在程序中就可以借助AssetManager这个类提供的接口对assets目录下的文件进行读取。

右击app/src/main→New→Directory,在弹出的对话框中输入“assets”,目录就创建好了,放一个mp3文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class MainActivity : AppCompatActivity() {
private val mediaPlayer = MediaPlayer()

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

play.setOnClickListener {
if (!mediaPlayer.isPlaying) {
mediaPlayer.start() // 开始播放
}
}

pause.setOnClickListener {
if (mediaPlayer.isPlaying) {
mediaPlayer.pause() // 暂停播放
}
}

stop.setOnClickListener {
if (mediaPlayer.isPlaying) {
mediaPlayer.reset() // 停止播放
initMediaPlayer()
}
}

}

private fun initMediaPlayer() {
val assetManager = assets
val fd = assetManager.openFd("music.mp3")
mediaPlayer.setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
mediaPlayer.prepare()
}

override fun onDestroy() {
super.onDestroy()
mediaPlayer.stop()
mediaPlayer.release()
}
}

9.3.2 播放视频

VideoView 工具类

image-20220725215336925

VideoView不支持直接播放assets目录下的视频资源
res目录下允许我们再创建一个raw目录,音频、视频之类的资源文件可以放在这里,VideoView是可以直接播放这个目录下的视频资源的。 mp4

新建一个项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<Button
android:id="@+id/play"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Play" />
<Button
android:id="@+id/pause"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Pause" />
<Button
android:id="@+id/replay"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Replay" />
</LinearLayout>
<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLay
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val uri = Uri.parse("android.resource://$packageName/${R.raw.video}")
videoView.setVideoURI(uri)

play.setOnClickListener {
if (!videoView.isPlaying) {
videoView.start() // 开始播放
}
}
pause.setOnClickListener {
if (videoView.isPlaying) {
videoView.pause() // 暂停播放
}
}

replay.setOnClickListener {
if (videoView.isPlaying) {
videoView.resume() // 重新播放
}
}
}
override fun onDestroy() {
super.onDestroy()
videoView.suspend()
}
}

十,Service

Service并不是运行在一个独立的进程当中的,而是依赖于创建Service时所在的 APP 进程。 当某个应用程序进程被杀掉时,所有依赖于该进程的Service也会停止运行。

10.1 Android 多线程编程

10.1.1 线程基本用法

1
2
3
thread {
// 编写具体的逻辑 从这里开始开启了一个子线程
}

演变和其他版本可以看参考教材《第一行代码 第三版》

10.1.2 在子线程中更新 UI

Android的UI也是线程不安全的。

Android不允许在子线程中操作UI

解决办法:异步消息处理机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/changeTextBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Change Text" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Hello world"
android:textSize="20sp" />
</RelativeLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.lmc.android45

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.widget.Button
import android.widget.TextView
import kotlin.concurrent.thread

class MainActivity : AppCompatActivity() {

val updateText = 1
val handler = object : Handler(Looper.getMainLooper()){
override fun handleMessage(msg: Message) {
when (msg.what){
//在这里进行UI操作
updateText -> findViewById<TextView>(R.id.textView).text = "你好,世界。"
}
}
}

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

//用异步消息处理机制
findViewById<Button>(R.id.changeTextBtn).setOnClickListener {
thread {
//这里面就是子线程啦
val msg = Message()
msg.what = updateText
handler.sendMessage(msg)//将Message对象发送出去
}
}
}
}

动画

10.1.3 解析异步消息处理机制

组成:Message、Handler、MessageQueue 和 Looper。

  1. Message
    线程之间传递的消息,内部携带少量的信息,用于在不同线程之间传递数据
    Message的what字段,除此之外还可以使用arg1和arg2字段来携带一些整型数据,使用obj字段携带一个Object对象。
  2. Handler
    Handler顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用Handler的sendMessage()方法、post()方法等,而发出的消息经过一系列地辗转处理后,最终会传递到Handler的handleMessage()方法中。
  3. MessageQueue
    MessageQueue 是消息队列的意思,它主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一 MessageQueue对象。
  4. Looper
    Looper 是每个线程中的 MessageQueue 的管家,调用 Looper 的 loop() 方法后,就会进入一个无限循环当中,然后每当发现MessageQueue中存在一条消息时,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中只会有一个Looper对象。

image-20220726104234370

异步消息处理流程:首先需要在主线程当中创建一个Handler对象,并重写handleMessage()方法。然后当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发回Handler的handleMessage()方法中。由于Handler的构造函数中我们传入了Looper.getMainLooper(),所以此时handleMessage()方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行UI操作了。

10.1.4 使用AsyncTask

用来取代啥子异步信息处理机制的

AsyncTask是抽象类,想使用就必须创建一个子类去继承。在继承时我们可以为AsyncTask类指定3个泛型参数

  • Params。在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。
  • Progress。在后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
  • Result。当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

这个抽象类需要重写的方法

  1. onPreExecute()
    在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。

  2. doInBackground(Params…)
    这个方法中的所有代码都会在 子线程 中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成,就可以通过return语句将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是Unit,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用 publishProgress (Progress…) 方法来完成。

  3. onProgressUpdate(Progress…)

    当在后台任务中调用了publishProgress(Progress…)方法后,onProgressUpdate (Progress…)方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。

  4. onPostExecute(Result)
    当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据进行一些UI操作,比如说提醒任务执行的结果,以及关闭进度条对话框等。

完整 Async Task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class DownloadTask : AsyncTask<Unit, Int, Boolean>() {

override fun onPreExecute() {
progressDialog.show() // 显示进度对话框
}

//只有我是在子线程中干活的
override fun doInBackground(vararg params: Unit?) = try {
while (true) {
val downloadPercent = doDownload() // 这是一个虚构的方法
publishProgress(downloadPercent)//就一句这个直接从子线程切换到UI线程
if (downloadPercent >= 100) {
break
}
}
true
} catch (e: Exception) {
false
}

override fun onProgressUpdate(vararg values: Int?) {
// 在这里更新下载进度
progressDialog.setMessage("Downloaded ${values[0]}%")
}

override fun onPostExecute(result: Boolean) {
progressDialog.dismiss()// 关闭进度对话框
// 在这里提示下载结果
if (result) {
Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, " Download failed", Toast.LENGTH_SHORT).show()
}
}
}

//启动这个任务
DownloadTask().execute()

简单来说,使用AsyncTask的诀窍就是,在doInBackground()方法中执行具体的耗时任务,在onProgressUpdate()方法中进行UI操作,在onPostExecute()方法中执行一些任务的收尾工作。

你也可以给 execute() 方法传入任意数量的参数,这些参数将会传递到 DownloadTask 的 doInBackground() 方法当中。

10.2 Service 基本用法

新建项目

10.2.1 定义一个 Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.lmc.android46

import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log

class MyService : Service() {
private val tag = "MyService"

//唯一抽象方法
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}

//第一次创建Service执行
override fun onCreate() {
super.onCreate()
Log.d(tag, "onCreating")
}

//启动Service时候调用
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(tag, "onStartCommanding")//这个必须在上面
return super.onStartCommand(intent, flags, startId)

}

//销毁Service时候调用
override fun onDestroy() {
super.onDestroy()
Log.d(tag, "onDestoring")
}
}

其中 onCreate() 方法会在 Service 创建的时候调用,onStartCommand() 方法会在每次Service 启动的时候调用,onDestroy() 方法会在 Service 销毁的时候调用。

10.2.2 启动和停止 Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.lmc.android46

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button

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

findViewById<Button>(R.id.startServiceBtn).setOnClickListener {
val intent = Intent(this,MyService::class.java)
startService(intent)//启动Service
}
findViewById<Button>(R.id.stopServiceBtn).setOnClickListener {
val intent = Intent(this,MyService::class.java)
stopService(intent)
}
}
}

Service 自我停止运行 = Service 内部调用stopSelf()

APP在前台可见时候 Service 才能保证稳定运行,一旦应用进入后台之后,Service随时都有可能被系统回收,解决办法可以使用前台Service或者WorkManager

image-20220928161639921

10.2.3 Activity和Service进行通信

利用 onBind 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyService : Service() {
private val mBinder = DownloadBinder()
class DownloadBinder : Binder() {
fun startDownload() {
Log.d("MyService", "startDownload executed")
}
fun getProgress(): Int {
Log.d("MyService", "getProgress executed")
return 0
}
}
override fun onBind(intent: Intent): IBinder {
return mBinder
}
...
}

xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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/bindServiceBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Bind Service" />
<Button
android:id="@+id/unbindServiceBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Unbind Service" />
</LinearLayout>

为了在Activity中控制Service1,可以把两者绑定起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MainActivity : AppCompatActivity() {

lateinit var downloadBinder: MyService.DownloadBinder

private val connection = object : ServiceConnection {

override fun onServiceConnected(name: ComponentName, service: IBinder) {//Activity与Service成功绑定的时候调用
downloadBinder = service as MyService.DownloadBinder//向下转型
//从这里开始可以从Activity中调用Service中的方法
downloadBinder.startDownload()
downloadBinder.getProgress()
}

override fun onServiceDisconnected(name: ComponentName) {
//在Service的创建进程崩溃或者被杀掉的时候
}

}//这个叫匿名类

override fun onCreate(savedInstanceState: Bundle?) {
...
findViewById<Button>(R.id.bindServiceBtn).setOnClickListener {
val intent = Intent(this, MyService::class.java)

//第三个参数则是一个标志位,这里传入BIND_AUTO_CREATE表示在Activity和Service进行绑定后自动创建Service。这会使得MyService中的onCreate()方法得到执行,但onStartCommand()方法不会执行
//也就是说这种的一绑定的话就会自动 create Service
bindService(intent, connection, Context.BIND_AUTO_CREATE) // 绑定Service
}
findViewById<Button>(R.id.unbindServiceBtn).setOnClickListener {
unbindService(connection) // 解绑Service
}
}
}

Service在整个应用程序范围内都是通用的,MyService可以和多个Activity进行绑定,而且在绑定完成后,它们都可以获取相同的DownloadBinder实例。

image-20220928173327494

10.3 Service 生命周期

从调用Context的startService()方法,相应的Service就会启动,并回调onStartCommand()方法。如果这个Service之前还没有创建过,onCreate()方法会先于onStartCommand()方法执行。Service启动了之后会一直保持运行状态,直到stopService()或stopSelf()方法被调用,或者被系统回收。注意,虽然每调用一次startService()方法,onStartCommand()就会执行一次,但实际上每个Service只会存在一个实例。也就是说他是重新开启自己,相当于刷新了一下。所以不管你调用了多少次startService()方法,只需调用一次stopService()或stopSelf()方法,Service就会停止。

调用Context的bindService()来获取一个Service 的持久连接,这时就会回调Service中的onBind()方法。类似地,如果这个Service之前还没有创建过,onCreate()方法会先于onBind()方法执行。之后,调用方可以获取到onBind()方法里返回的IBinder对象的实例,这样就能自由地和Service进行通信了。只要调用方和Service之间的连接没有断开,Service就会一直保持运行状态,直到被系统回收。
当调用了startService()方法后,再去调用stopService()方法。这时Service中的
onDestroy()方法就会执行,表示Service已经销毁了。类似地,当调用了bindService()方法后,再去调用unbindService()方法,onDestroy()方法也会执行,这两种情况都很好理解。

但是需要注意,我们是完全有可能对一个Service既调用了startService()方法,又调用了bindService()方法的,在这种情况下该如何让Service销毁呢?根据Android系统的机制,一个Service只要被启动或者被绑定了之后,就会处于运行状态,必须要让以上两种条件同时不满足,Service才能被销毁。所以,这种情况下要同时调用stopService()和unbindService()方法,onDestroy()方法才会执行。

10.4 Service 更多技巧

10.4.1 使用前台 Service

前台Service :Android 8.0之后希望 Service 能够一直保持运行状态。
前台 Service 和普通 Service 最大的区别就在于,它一直会有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyService : Service() {
...
override fun onCreate() {
super.onCreate()
Log.d("MyService", "onCreate executed")
//下面是加的东西
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as
NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel("my_service", "前台Service通知",
NotificationManager.IMPORTANCE_DEFAULT)
manager.createNotificationChannel(channel)
}

val intent = Intent(this, MainActivity::class.java)
val pi = PendingIntent.getActivity(this, 0, intent, 0)
val notification = NotificationCompat.Builder(this, "my_service")
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
.setContentIntent(pi)
.build()
//上面都是通知的套路,就这里改了一下子
startForeground(1, notification)
}
...
}

另外,从Android 9.0系统开始,使用前台Service必须在AndroidManifest.xml文件中进行权限声明才行

1
2
3
4
5
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicetest">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
...
</manifest>

10.4.2 使用 IntentService

Service中的代码都是默认运行在主线程当中的,如果直接在Service里处理一些耗时的逻辑,就很容易出现ANR(Application Not Responding)的情况。所以这个时候就需要用到 Android 多线程编程的技术了,我们应该在Service的每个具体的方法里开启一个子线程,然后在这里处理那些耗时的逻辑。

本来一套逻辑

1
2
3
4
5
6
7
8
9
10
class MyService : Service() {
...
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
thread {
// 处理具体的逻辑
stopSelf()
}
return super.onStartCommand(intent, flags, startId)
}
}

为了可以简单地创建一个异步的、会自动停止的Service,Android专门提供了一个 IntentService 类

IntentService

1
2
3
4
5
6
7
8
9
10
11
class MyIntentService : IntentService("MyIntentService") {//构造字符串随意好了
override fun onHandleIntent(intent: Intent?) {
// 打印当前线程的id
Log.d("MyIntentService", "Thread id is ${Thread.currentThread().name}")
}

override fun onDestroy() {
super.onDestroy()
Log.d("MyIntentService", "onDestroy executed")
}
}

然后要在子类中实现 onHandleIntent() 这个抽象方法,这个方法中可以处理一些耗时的逻辑,而不用担心ANR的问题,因为这个方法已经是在子线程中运行的了。这里为了证实一下,我们在 onHandleIntent() 方法中打印了当前线程名。另外,根据 IntentService 的特性,这个 Service 在运行结束后应该是会自动停止的,所以我们又重写了 onDestroy() 方法,在这里也打印了一行日志,以证实 Service 是不是停止了。

验证工作:

1
2
3
4
5
6
7
8
9
10
11
<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/startIntentServiceBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start IntentService" />
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
startIntentServiceBtn.setOnClickListener {
// 打印主线程的id
Log.d("MainActivity", "Thread id is ${Thread.currentThread().name}")
val intent = Intent(this, MyIntentService::class.java)
startService(intent)
}
}
}

注意,Service 有没有在AndroidManifest 中注册(手动自动)

十一,网络

11.1 WebView

一个WebView控件,借助它我们就可以在自己的应用程序里嵌入一个浏览器,从而非常轻松地展示各种各样的网页。

新建一个项目

1
2
3
4
5
6
7
8
9
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.lmc.android50

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient

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

val webView = findViewById<WebView>(R.id.webView)
webView.settings.javaScriptEnabled = true//这个网页能够使用 JS
webView.webViewClient = WebViewClient()//当需要网页跳转的时候,直接在当前WebView中显示
webView.loadUrl("https://www.baidu.com")
}
}

访问网络是需要申请权限的

1
2
3
4
5
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.webviewtest">
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>

Screenshot_20221004_164201_com.lmc.android50

11.2 手动使用 HTTP 访问网络

客户端向服务器发出一条HTTP请求,服务器收到请求之后会返回一些数据给客户端,然后客户端再对这些数据进行解析和处理就可以了。

11.2.1 使用HttpURLConnection

Android上发送HTTP请求

1
2
3
4
5
6
7
8
val url = URL("https://www.baidu.com")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connectTimeout = 8000
connection.readTimeout = 8000
val input = connection.inputStream
//这个地方就可以对浏览器返回的输入流进行读取
connection.disconnect()
  1. 首先需要获取HttpURLConnection的实例,一般只需创建一个URL对象,并传入目标的网络地址,然后调用一下openConnection()方法即可
  2. 在得到了HttpURLConnection的实例之后,我们可以设置一下HTTP请求所使用的方法。常用的方法主要有两个:GET和POST。GET表示希望从服务器那里获取数据,而POST则表示希望提交数据给服务器。
  3. 接下来就可以进行一些自由的定制了,比如设置连接超时、读取超时的毫秒数,以及服务器希望得到的一些消息头等。
  4. 之后再调用getInputStream()方法就可以获取到服务器返回的输入流了,剩下的任务就是对输入流进行读取
  5. 最后可以调用disconnect()方法将这个HTTP连接关闭

ScrollView。它是用来做什么的呢?由于手机屏幕的空间一般比较小,有些时候过多的内容一屏是显示不下的,借助ScrollView控件,我们就可以以滚动的形式查看屏幕外的内容。

一个手动访问服务器的小演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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/sendRequestBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send Request" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/responseText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

sendRequestBtn.setOnClickListener {
sendRequestWithHttpURLConnection()
}
}
private fun sendRequestWithHttpURLConnection() {
// 开启线程发起网络请求
thread {
var connection: HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL("https://www.baidu.com")
connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 8000
connection.readTimeout = 8000
val input = connection.inputStream
// 下面对获取到的输入流进行读取
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
response.append(it)
}
}
showResponse(response.toString())
} catch (e: Exception) {
e.printStackTrace()
} finally {
connection?.disconnect()
}
}
}
private fun showResponse(response: String) {
runOnUiThread {
// 在这里进行UI操作,将结果显示到界面上
responseText.text = response
}
}
}
1
2
3
4
5
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.networktest">
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>

动画

向服务器提交数据

只需要将HTTP请求的方法改成POST,并在获取输入流之前把要提交的数据写出即可。注意,每条数据都要以键值对的形式存在,数据与数据之间用“&”符号隔开。

1
2
3
connection.requestMethod = "POST"
val output = DataOutputStream(connection.outputStream)
output.writeBytes("username=admin&password=123456")

11.2.2 使用OkHttp

  1. app/build.gradle 加上依赖包
1
2
3
4
dependencies {
...
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
}
  1. 首先需要创建一个OkHttpClient
  2. 创建一个Request对象
  3. 调用OkHttpClient的newCall()方法来创建一个Call对象,并调用它的execute()方法
    来发送请求并获取服务器返回的数据
  4. 得到返回的具体内容

GET:

1
2
3
4
5
6
val client = OkHttpClient()
val request = Request.Builder()
.url("https://www.baidu.com")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()

POST:

1
2
3
4
5
6
7
8
9
10
11
12
val client = OkHttpClient()
//存放待提交的参数
val requestBody = FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build()
val request = Request.Builder()
.url("https://www.baidu.com")
.post(requestBody)
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()

用这个 OkHttp 优化之前用 HttpURLConnection 的哪一个 Android50二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MainActivity : AppCompatActivity() {

...
//就下面这个函数的实现改用O
private fun sendRequestWithOkHttp() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://www.baidu.com")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
showResponse(responseData)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
...
}

11.3 解析XML格式数据

辣个 Apache 搭建说一下,《第一行代码 第三版》上说的,这边试过是完全用不了的。我写了一个博客实测是可行的,各种遇到的问题也在里面收集了解决方案。

后面看情况上传公众号或者B站专栏。

11.3.0 Apache 准备

这里一会我把来链接贴出来:

进入C:\Apache\htdocs目录下,在这里新建一个名为get_data.xml的文件,然后编辑
这个文件,并保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<apps>
<app>
<id>1</id>
<name>Google Maps</name>
<version>1.0</version>
</app>

<app>
<id>2</id>
<name>Chrome</name>
<version>2.1</version>
</app>

<app>
<id>3</id>
<name>Google Play</name>
<version>2.3</version>
</app>
</apps>

然后再在浏览器中访问localhost:8088/get_data.xml

image-20221004213621025

空白处右击选择查看页面源代码后

image-20221004213652386

11.3.1 Pull解析方式

说一下下面第29行代码的注意:这个地方看视频怎末说的。是个大坑,稍不注意就会出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.lmc.android50

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.TextView
import okhttp3.OkHttpClient
import okhttp3.Request
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.StringReader
import java.lang.Exception
import java.net.CacheResponse
import java.net.HttpURLConnection
import java.net.URL
import kotlin.concurrent.thread

class MainActivity : AppCompatActivity() {
...
private fun sendRequestWithHttpURLConnection(){
//用OkHttp改造过的
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
//注意这里的ip每个人都不一样,看下面我说的情况,和视频。
.url("http://ip/get_data.xml")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
//showResponse(responseData)
//读取Apache服务器发过来的额xml并进行Pull解析
parseXMLWithPull(responseData)

}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
...
private fun parseXMLWithPull(xmlData: String) {
try {
val factory = XmlPullParserFactory.newInstance()
val xmlPullParser = factory.newPullParser()
xmlPullParser.setInput(StringReader(xmlData))//将服务器返回的XML数据设置进去
//开始解析
var eventType = xmlPullParser.eventType//得到当前解析事件
var id = ""
var name = ""
var version = ""
while (eventType != XmlPullParser.END_DOCUMENT) {
val nodeName = xmlPullParser.name//拿到节点名

when (eventType) {
// 开始解析某个节点
XmlPullParser.START_TAG -> {
when (nodeName) {
"id" -> id = xmlPullParser.nextText()
"name" -> name = xmlPullParser.nextText()
"version" -> version = xmlPullParser.nextText()
}
}
// 完成解析某个节点
XmlPullParser.END_TAG -> {
if ("app" == nodeName) {
Log.d("MainActivity", "id is $id")
Log.d("MainActivity", "name is $name")
Log.d("MainActivity", "version is $version")
}
}
}
eventType = xmlPullParser.next()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

那么为了能让程序使用HTTP,我们还要进行如下配置才可以。在res目录下右击xml目录→New→File,创建一个network_config.xml文件。然后修改network_config.xml文件中的内容,如下所示:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:android="http://schemas.android.com/apk/res/android">
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>

这段配置文件的意思就是允许我们以明文的方式在网络上传输数据,而HTTP使用的就是明文传输方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.lmc.android50">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Android50二"
android:networkSecurityConfig="@xml/network_config"
tools:targetApi="31">
...
</application>

</manifest>

就加了一个 networkSecurityConfig

结果:

image-20221005180418600

11.2.3.2 SAX解析方式

要使用SAX解析,通常情况下我们会新建一个类继承自DefaultHandler,并重写父类的5个
方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyHandler : DefaultHandler() {
//开始XML解析的时候调用
override fun startDocument() {
}
//开始解析某个节点的时候调用
override fun startElement(uri: String, localName: String, qName: String, attributes:
Attributes) {
}
//在获取节点中内容的时候调用
override fun characters(ch: CharArray, start: Int, length: Int) {
}
//完成解析某个节点的时候调用
override fun endElement(uri: String, localName: String, qName: String) {
}
//在完成整个XML解析的时候调用
override fun endDocument() {
}
}

其中,startElement()、characters()和endElement()这3个方法是有参数的,从XML中解析出的数据就会以参数的形式传入这些方法中。需要注意的是,在获取节点中的内容时,characters()方法可能会被调用多次,一些换行符也被当作内容解析出来,我们需要针对这种情况在代码中做好控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class ContentHandler : DefaultHandler() {
private var nodeName = ""
private lateinit var id: StringBuilder
private lateinit var name: StringBuilder
private lateinit var version: StringBuilder
//下面开始XML解析
override fun startDocument() {
id = StringBuilder()
name = StringBuilder()
version = StringBuilder()
}
//开始一个节点的解析
override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
// 记录当前节点名
nodeName = localName//这就是节点名
Log.d("ContentHandler", "uri is $uri")
Log.d("ContentHandler", "localName is $localName")
Log.d("ContentHandler", "qName is $qName")
Log.d("ContentHandler", "attributes is $attributes")
}
override fun characters(ch: CharArray, start: Int, length: Int) {
// 根据当前节点名判断将内容添加到哪一个StringBuilder对象中
when (nodeName) {
"id" -> id.append(ch, start, length)
"name" -> name.append(ch, start, length)
"version" -> version.append(ch, start, length)
}
}
override fun endElement(uri: String, localName: String, qName: String) {
if ("app" == localName) {
Log.d("ContentHandler", "id is ${id.toString().trim()}")
Log.d("ContentHandler", "name is ${name.toString().trim()}")
Log.d("ContentHandler", "version is ${version.toString().trim()}")
// 最后要将StringBuilder清空
id.setLength(0)
name.setLength(0)
version.setLength(0)
}
}
override fun endDocument() {
}
}


class MainActivity : AppCompatActivity() {
...
private fun sendRequestWithOkHttp() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
// 指定访问的服务器地址是计算机本机
.url("http://10.129.30.95/get_data.xml")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
parseXMLWithSAX(responseData)//这个方法就这里改掉了
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
...
private fun parseXMLWithSAX(xmlData: String) {
try {
val factory = SAXParserFactory.newInstance()
val xmlReader = factory.newSAXParser().xmlReader//第一行代码这个地方有错哈,改成小写全部
val handler = ContentHandler()
// 将ContentHandler的实例设置到XMLReader中
xmlReader.contentHandler = handler
// 开始执行解析
xmlReader.parse(InputSource(StringReader(xmlData)))
} catch (e: Exception) {
e.printStackTrace()
}
}
}

image-20221006110718955

11.2.3.3 DOW解析方式

自己有兴趣再去了解

11.4 解析 JSON 格式数据

比起XML,JSON的主要优势在于它的体积更小,在网络上传输的时候更省流量。但缺
点在于,它的语义性较差,看起来不如XML直观。

C:\Apache24\htdocs目录中新建一个get_data.json的文件

1
2
3
[{"id":"5","version":"5.5","name":"Clash of Clans"},
{"id":"6","version":"7.0","name":"Boom Beach"},
{"id":"7","version":"3.5","name":"Clash Royale"}]

11.4.1 使用JSONObject

官方的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class MainActivity : AppCompatActivity() {
...
private fun sendRequestWithOkHttp() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
// 指定访问的服务器地址是计算机本机
.url("http://10.129.30.95/get_data.json")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
parseJSONWithJSONObject(responseData)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
...
private fun parseJSONWithJSONObject(jsonData: String) {
try {
val jsonArray = JSONArray(jsonData)
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
val id = jsonObject.getString("id")
val name = jsonObject.getString("name")
val version = jsonObject.getString("version")
Log.d("MainActivity", "id is $id")
Log.d("MainActivity", "name is $name")
Log.d("MainActivity", "version is $version")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

首先将HTTP请求的地址改成http://10.0.2.2/get_data.json,然后在得到服务器返回的数据后调用parseJSONWithJSONObject()方法来解析数据。可以看到,解析JSON的代码真的非常简单,由于我们在服务器中定义的是一个JSON数组,因此这里首先将服务器返回的数据传入一个JSONArray对象中。然后循环遍历这个JSONArray,从中取出的每一个元素都是一个JSONObject对象,每个JSONObject对象中又会包含id、name和version这些数据。接下来只需要调用getString()方法将这些数据取出,并打印出来即可。

image-20221006114138620

11.4.2 使用GSON

谷歌开源库

1
2
3
4
dependencies {
...
implementation 'com.google.code.gson:gson:2.9.1'
}

image-20220727170430667

image-20220727170607320

实操

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//新增一个App类
class App(val id: String, val name: String, val version: String)

class MainActivity : AppCompatActivity() {
...
private fun sendRequestWithOkHttp() {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder()
// 指定访问的服务器地址是计算机本机
.url("http://10.129.30.95/get_data.json")
.build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
parseJSONWithGSON(responseData)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
...
private fun parseJSONWithGSON(jsonData: String) {
val gson = Gson()
val typeOf = object : TypeToken<List<App>>() {}.type
val appList = gson.fromJson<List<App>>(jsonData, typeOf)
for (app in appList) {
Log.d("MainActivity", "id is ${app.id}")
Log.d("MainActivity", "name is ${app.name}")
Log.d("MainActivity", "version is ${app.version}")
}
}
}

image-20221006120034951

还有 Jackson,FastJSON 等

11.5 网络请求回调的实现方式

通常情况下我们应该将这些通用的网络操作提取到一个公共的类里,并提供一个通用方法,当想要发起网络请求的时候,只需简单地调用一下这个方法即可。

来用 HttpURLConnection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//下面是一个不正确的临时演示代码
object HttpUtil {//这个其实是一个静态单例类
fun sendHttpRequest(address: String): String {
var connection: HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL(address)
connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 8000
connection.readTimeout = 8000
val input = connection.inputStream
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
response.append(it)
}
}
return response.toString()
} catch (e: Exception) {
e.printStackTrace()
return e.message.toString()
} finally {
connection?.disconnect()
}
}
}

//以后每当需要发起一条HTTP请求的时候,就可以这样写:
val address = "https://www.baidu.com"
val response = HttpUtil.sendHttpRequest(address)

看起来没有问题,但实际是不可行的。网络请求通常属于耗时操作,而sendHttpRequest()方法的内部并没有开启线程,这样就有可能导致在调用sendHttpRequest()方法的时候主线程被阻塞。

那在sendHttpRequest()方法内部开启一个线程?如果我们在sendHttpRequest()方法中开启一个线程来发起HTTP请求,服务器响应的数据是无法进行返回的。这是由于所有的耗时逻辑都是在子线程里进行的,sendHttpRequest()方法会在服务器还没来得及响应的时候就执行结束了,当然也就无法返回响应的数据了。

解决方法-》编程语言的回调机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//这个是原始版本的OK的代码演示
interface HttpCallbackListener {
fun onFinish(response: String)
fun onError(e: Exception)
}



object HttpUtil {
fun sendHttpRequest(address: String, listener: HttpCallbackListener) {
thread {
var connection: HttpURLConnection? = null
try {
val response = StringBuilder()
val url = URL(address)
connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 8000
connection.readTimeout = 8000
val input = connection.inputStream
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
response.append(it)
}
}
// 回调onFinish()方法
listener.onFinish(response.toString())
} catch (e: Exception) {
e.printStackTrace()
// 回调onError()方法
listener.onError(e)
} finally {
connection?.disconnect()
}
}
}
}

//用的时候演示
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(response: String) {
// 得到服务器返回的具体内容
}
override fun onError(e: Exception) {
// 在这里对异常情况进行处理
}
})

注意,子线程中是无法通过 return 语句返回数据的,因此这里我们将服务器响应的数据传入了 HttpCallbackListener 的 onFinish() 方法中,如果出现了异常,就将异常原因传入 onError() 方法

如此一来,我们就巧妙地利用回调机制将响应数据成功返回给调用方了。

用 OkHttp 会简单一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
object HttpUtil {
...
fun sendOkHttpRequest(address: String, callback: okhttp3.Callback) {
val client = OkHttpClient()
val request = Request.Builder()
.url(address)
.build()
client.newCall(request).enqueue(callback)//这个地方改了一点东西
//OkHttp在enqueue()方法的内部已经帮我们开好子线程了,然后会在子线程中执行HTTP请求,并将最终的请求结果回调到okhttp3.Callback当中。
}
}


HttpUtil.sendOkHttpRequest(address, object : Callback {
override fun onResponse(call: Call, response: Response) {
// 得到服务器返回的具体内容
val responseData = response.body?.string()
}
override fun onFailure(call: Call, e: IOException) {
// 在这里对异常情况进行处理
}
})

需要注意的是,不管是使用HttpURLConnection还是OkHttp,最终的回调接口都还是
在子线程中运行的,因此我们不可以在这里执行任何的UI操作,除非借助runOnUiThread()方法来进行线程转换

11.6 最好用的网络库:Retrofit

新建一个项目

11.6.1 基本用法

加上依赖

1
2
3
4
5
dependencies {
...
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
}

由于Retrofit是基于OkHttp开发的,因此添加上述第一条依赖会自动将Retrofit、OkHttp和Okio这几个库一起下载,我们无须再手动引入OkHttp库。另外,Retrofit还会将服务器返回的JSON数据自动解析成对象,因此上述第二条依赖就是一个Retrofit的转换库,它是借助GSON来解析JSON数据的,所以会自动将GSON库一起下载下来,这样我们也不用手动引入GSON库了。除了GSON之外,Retrofit还支持各种其他主流的JSON解析库,包括Jackson、Moshi等,不过毫无疑问GSON是最常用的 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class App(val id: String, val name: String, val version: String)

//由于我们的Apache服务器上其实只有一个获取JSON数据的接口,因此这里只需要定义一个接口文件,并包含一个方法即可
interface AppService {//通常Retrofit的接口文件建议以具体的功能种类名开头,并以Service结尾,这是一种比较好的命名习惯。
@GET("get_data.json")//这个地方前面是说我们请求的类型,后面则是一个相对路径
fun getAppData(): Call<List<App>>//getAppData()方法的返回值必须声明成Retrofit中内置的Call类型,并通过泛型来指定服务器响应的数据应该转换成什么对象
}


class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getAppDataBtn.setOnClickListener {
val retrofit = Retrofit.Builder()
.baseUrl("http://10.129.30.95/")//指定所有Retrofit请求的根路径
.addConverterFactory(GsonConverterFactory.create())//指定解析数据时用的转换库
.build()
val appService = retrofit.create(AppService::class.java)//创建一个该接口的动态代理对象,有了这个对象就可以去调用接口里面的方法

appService.getAppData().enqueue(object : Callback<List<App>> {
//发起请求的时候,Retrofit会自动在内部开启子线程,当数据回调到Callback中之后,Retrofit又会自动切换回主线程,整个操作过程中我们都不用考虑线程切换问题。
override fun onResponse(call: Call<List<App>>,
response: Response<List<App>>) {
val list = response.body()//得到Retrofit解析后的对象,,也就是List<App>类型的数据
if (list != null) {
for (app in list) {
Log.d("MainActivity", "id is ${app.id}")
Log.d("MainActivity", "name is ${app.name}")
Log.d("MainActivity", "version is ${app.version}")
}
}
}
override fun onFailure(call: Call<List<App>>, t: Throwable) {
t.printStackTrace()
}
})
}
}
}

网络安全配置:复制 network_config.xml 文件到 Retrofit项目当中,然后修改AndroidManifest.xml中的代码

设置了允许使用明文的方式来进行网络请求,同时声明了网络权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.retrofittest">
<uses-permission android:name="android.permission.INTERNET" />
<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"
android:networkSecurityConfig="@xml/network_config">
...
</application>
</manifest>

11.6.2 处理复杂的接口地址类型

上一个小节,我们向一个非常简单的服务器接口地址发送请求。但实际中这个是很复杂的。

准备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//1.最简单的静态服务器接口地址
//定义一个Data类
class Data(val id: String, val content: String)
GET http://example.com/get_data.json
//对应到Retrofit中的写法
interface ExampleService {
@GET("get_data.json")
fun getData(): Call<Data>
}


//2.动态接口地址
GET http://example.com/<page>/get_data.json
//上面那个page就是第几页 控制变化办法在下面
interface ExampleService {
@GET("{page}/get_data.json")
fun getData(@Path("page") page: Int): Call<Data>
}


//3.服务器接口要求带参数
GET http://example.com/get_data.json?u=<user>&t=<token>
interface ExampleService {
@GET("get_data.json")
fun getData(@Query("u") user: String, @Query("t") token: String): Call<Data>
}

HTTP类型:GET请求用于从服务器获取数据,POST请求用于向服务器提交数据,PUT和PATCH请求用于修改服务器上的数据,DELETE请求用于删除服务器上的数据。

而Retrofit对所有常用的HTTP请求类型都进行了支持,使用@GET、@POST、@PUT、@PATCH、@DELETE注解,就可以让Retrofit发出相应类型的请求了

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//1. 删除
//服务器提供以下接口地址
DELETE http://example.com/data/<id>
//在Retrofit发送请求
interface ExampleService {
@DELETE("data/{id}")
fun deleteData(@Path("id") id: String): Call<ResponseBody>
//ResponseBody,表示Retrofit能够接收任意类型的响应数据,并且不会对
响应数据进行解析
}


//2.提交数据
POST http://example.com/data/create
{"id": 1, "content": "The description for this data."}

interface ExampleService {
@POST("data/create")
fun createData(@Body data: Data): Call<ResponseBody>
}

这里我们在createData()方法中声明了一个Data类型的参数,并给它加上了@Body注解。这样当Retrofit发出POST请求时,就会自动将Data对象中的数据转换成JSON格
式的文本,并放到HTTP请求的body部分,服务器在收到请求之后只需要从body中将这部分数据解析出来即可。这种写法同样也可以用来给PUT、PATCH、DELETE类型的请求提交数据。

服务器接口要求HTTP请求的header中指定参数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//静态header声明
GET http://example.com/get_data.json
User-Agent: okhttp
Cache-Control: max-age=0

interface ExampleService {
@Headers("User-Agent: okhttp", "Cache-Control: max-age=0")
@GET("get_data.json")
fun getData(): Call<Data>
}



//动态header声明
interface ExampleService {
@GET("get_data.json")
fun getData(@Header("User-Agent") userAgent: String,
@Header("Cache-Control") cacheControl: String): Call<Data>
}
//实现了动态指定header值

11.6.3 Retrofit构建器的最佳写法

开始的写法

1
2
3
4
5
val retrofit = Retrofit.Builder()
.baseUrl("http://10.0.2.2/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val appService = retrofit.create(AppService::class.java)
1
2
3
4
5
6
7
8
9
10
11
object ServiceCreator {//单例类
private const val BASE_URL = "http://10.0.2.2/"//指定Retrofit的根路径
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
inline fun <reified T> create(): T = create(T::class.java)
}

//用上面说的来获取一个AppService接口的动态管理对象
val appService = ServiceCreator.create<AppService>()

十二,Material Design

Android最优雅最美的UI设计 Material Design。

12.1 Toolbar

每个Activity最顶部的那个标题栏其实就是ActionBar,Toolbar是他的MD升级版本。

APP是可以指定主题的(看现在主题直接去manifest,Ctrl+B就可以找到)

12.1.1 换上Toolbar!

12.1.1.1 换个没有ActionBar的主题

我们想用Toolbar代替ActionBat。所以要找个不带ActionBar的主题,通常 Theme.AppCompat.NoActionBar 和Theme.AppCompat.Light.NoActionBar这两种主题可选。(其实最新版本AS默认就是MD主题)

主题换成这个亚子:(是个浅色主题)

1
2
3
4
5
6
7
8
9
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/design_default_color_primary</item>
<item name="colorPrimaryDark">@color/design_default_color_primary_dark</item>

</style>
</resources>

colorPrimary是顶部栏,然后后面带Dark的是最顶端的状态栏。

12.1.1.2 改activity_main.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/design_default_color_primary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

</FrameLayout>

为了能够兼容老系统我们使用xmlns:app指定了一个新的命名空间(其实人家一直都有)

制定了 xmlns:app 后面才能 app:啥子

不然就只能 android:啥子

android:layout_height="?attr/actionBarSize":高度设置为actionBar的高度

最后两行设置主题的:因为APP是浅色,然后这个Toolbar也会是浅色,那它上面的字啥的就会是深色,有点丑。所以我们单独设置这个Toolbar为深色,这样上面的labal就是以前的白色了。但是这样到时候Toolbar上弹出的小菜单(就是那三个点)就也是白色了,就在APP的浅色中看不见。所以又单独给弹出的菜单设置为浅色主题,这样他上面的菜单字就是黑的,又可以看见了。

挺有意思的,不信你去改改?

给你看下没这个的后果:

  1. 啥都不加:image-20221022215528387
  2. 就加第一行:image-20221022220300454
12.1.1.3 改MainActivity
1
2
3
4
5
6
7
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
}
}

跑一下,虽然看起来没有啥变化但是它已经从 ActionBar 变身为 Toolbar(虽然在你AS上跑本来就是Toolbar)


下面这个没用下次可以删了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.lmc.android121

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
}

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.toolbar,menu)//这里是加载了toolbar.xml这个菜单文件
return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId){
R.id.backup -> {
Toast.makeText(this,"你要返回吗?",Toast.LENGTH_SHORT).show()
}
R.id.delete -> {
Toast.makeText(this,"删除还没有做好的",Toast.LENGTH_SHORT).show()
}
R.id.settings ->{
Toast.makeText(this,"这里是设置",Toast.LENGTH_SHORT).show()
}
}

return true
}
}

12.1.2 Toolbar的特有功能

12.1.2.1 改Toolbar(标题栏)上的 slogan(标语)

其实这个就是APP名

来到 AndroidManifest.xml ,在中找到".MainActivity"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.lmc.android121">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Android121"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="火山科技工作室">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

android:label属性,用于指定在Toolbar中显示的文字内容

image-20221022221647226

12.1.2.2 加点action按钮

把图片文件拉过来放在了drawable-xxhdpi目录,

右击res目录→New→Directory,创建一个menu文件夹。

然后右击menu文件夹→New→Menu resource file,创建一个toolbar.xml文件

文件中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/backup"
android:icon="@drawable/files"
android:title="文件"
app:showAsAction="always" />
<item
android:id="@+id/delete"
android:icon="@drawable/like"
android:title="喜欢"
app:showAsAction="ifRoom" />
<item
android:id="@+id/settings"
android:icon="@drawable/photo"
android:title="咔嚓"
app:showAsAction="never" />
</menu>

app:showAsAction来指定按钮的显示位置 ,

  • always表示永远显示在Toolbar中,如果屏幕空间不够则不显示;

  • ifRoom表示屏幕空间足够的情况下显示在Toolbar中,不够的话就显示在菜单当中;

  • never则表示永远显示在菜单当中。

注意,Toolbar中的action按钮只会显示图标,菜单中的action按钮只会显示文字。

然后修改MainActivity中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.lmc.android121

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
}

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.toolbar,menu)//这里是加载了toolbar.xml这个菜单文件
return true
}

//对Toolbar上按钮的点击事件进行处理
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val drawerLayout =
findViewById<androidx.drawerlayout.widget.DrawerLayout>(R.id.drawerLayout)
when (item.itemId) {
android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)
R.id.files -> {
Toast.makeText(this, "莫得文件", Toast.LENGTH_SHORT).show()
}
R.id.like -> {
Toast.makeText(this, "谢谢你的喜欢", Toast.LENGTH_SHORT).show()
}
R.id.photo -> {
Toast.makeText(this, "我暂时还拍不了照", Toast.LENGTH_SHORT).show()
}
}

return true
}
}

意外:

image-20221019193822195

12.2.3 Toolbar最后成果图

image-20221022230113157

12.2 滑动菜单

就是把一些菜单选项隐藏起来,而不是放在主屏幕上,然后可以通过滑动现实出来。就是QQ的辣个个人界面一样。

12.2.1 DrawerLayout(抽屉布局)

抽屉布局:第一个子控件是主屏幕中显示的内容,第二个子控件是滑动菜单中显示的内容。(实现滑动菜单的最外面保障)

12.2.1.1 换上抽屉布局

改activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
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:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/design_default_color_primary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

</FrameLayout>

<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#FFF"
android:text="This is menu"
android:textSize="30sp" />

</androidx.drawerlayout.widget.DrawerLayout>

解释一哈:

我们把最外面替换成了抽屉布局,然后主屏幕控件是一个FrameLayout,里面还是我们的 Toolbar ;第二个滑动菜单中显示的内容是一个 TextView 。

关键注意:第二个控件一定要加 layout_gravity,这个是决定到时候滑动菜单从哪个地方弹出来。(而且这个地方,他没有自动补全,你只能复制我的或者一个字母一个字母的打出来)

12.2.1.2 加滑动菜单的导航按钮

为了多一个点击弹出来菜单按钮,因为有些同学不知道这个地方会有菜单。

在drawable-xxhdpi目录下准备一张图标,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))//让toolbar成为新的标题wang'zhe
//滑动菜单的导航图标
supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true)//让导航按钮显示出来
it.setHomeAsUpIndicator(R.drawable.ic_menu)//将Home按钮改为一个导航按钮图标
}
}
...
//对Toolbar上按钮的点击事件进行处理
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
//Home按钮被点了就打开滑动菜单
android.R.id.home -> drawerLayout.openDrawer(GravityCompat.START)//XML里面写的抽屉菜单是 START 打开,所以这里要写START

...
}
return true
}
}

然后就差不多了:

image-20221023113245906

12.2.2 NavigationView(导航页面)

12.2.2.1 引入圆角库
1
2
3
4
dependencies {
...
implementation 'de.hdodenhof:circleimageview:3.0.1'
}

这里添加了一行依赖关系:是一个开源项目CircleImageView,实现图片圆形化的功能

因为现在AS一般自带MetialDesign,所以不用自己加

12.2.2.2 换主题

将 res/values/themes.xml 文件中 AppTheme 的 parent 主题改成Theme.MaterialComponents.Light.NoActionBar,否则在使用接下来的一些控件时可能会遇到崩溃问题。 image-20221023175127939


12.2.2.3 准备menu

素材图片放在drawablexxhdpi目录下,

右击menu文件夹→New→Menu resource file,创建一个nav_menu.xml文件,并编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/navCall"
android:icon="@drawable/nav_call"
android:title="Call" />
<item
android:id="@+id/navFriends"
android:icon="@drawable/nav_friends"
android:title="Friends" />
<item
android:id="@+id/navLocation"
android:icon="@drawable/nav_location"
android:title="Location" />
<item
android:id="@+id/navMail"
android:icon="@drawable/nav_mail"
android:title="Mail" />
<item
android:id="@+id/navTask"
android:icon="@drawable/nav_task"
android:title="Tasks" />
</group>
</menu>
12.2.2.4 准备 headerLayout

图片还是放在老地方:drawablexxhdpi

右击layout文件夹→New→Layout resource file,创建一个nav_header.xml文件。
修改其中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="180dp"
android:padding="10dp"
android:background="@color/design_default_color_primary">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/iconImage"
android:layout_width="70dp"
android:layout_height="70dp"
android:src="@drawable/me"
android:layout_centerInParent="true"/>
<TextView
android:id="@+id/mailText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="sb.qq.com"
android:textColor="#FFF"
android:textSize="14sp"/>
<TextView
android:id="@+id/userText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/mailText"
android:text="李老板"
android:textColor="#FFF"
android:textSize="14sp"/>
</RelativeLayout>

然后修改activity_main.xml中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout 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:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/design_default_color_primary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

</FrameLayout>

<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/nav_header"
app:menu="@menu/nav_menu" />

</androidx.drawerlayout.widget.DrawerLayout>

菜单点击事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true)
it.setHomeAsUpIndicator(R.drawable.ic_menu)
}
navView.setCheckedItem(R.id.navCall)
navView.setNavigationItemSelectedListener {
drawerLayout.closeDrawers()
true
}
}
...
}

12.3 悬浮按钮和可交互提示

12.3.1 FloatingActionButton

首先放一张 good.png到drawable-xxhdpi目录下。然后修改activity_main.xml中的代码 ,在FrameLayout中加FloatingActionButton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout 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:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/design_default_color_primary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16sp"
android:src="@drawable/good" />

</FrameLayout>

……

</androidx.drawerlayout.widget.DrawerLayout>

image-20221021160411192

其实这个东西的悬浮高度也可以指定:就是最后一行(其实感觉不大)

1
2
3
4
5
6
7
8
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16sp"
android:src="@drawable/good"
app:elevation="8dp"/>

给这个 FloatingActionButton 加点击按钮,MainActivity 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
//让悬浮按钮有点击效果
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener {
//Toast.makeText(this,"谢谢你的喜欢",Toast.LENGTH_SHORT).show()
Snackbar.make(it, "谢谢你的喜欢~", Snackbar.LENGTH_SHORT)
.setAction("你是有点东西的") {
Toast.makeText(this, "啊对", Toast.LENGTH_SHORT).show()
}
.show()
}
...
}

12.3.2 Snackbar

比Toast更厉害的提示工具,修改MainActivity中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
//让悬浮按钮有点击效果
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener {
//Toast.makeText(this,"谢谢你的喜欢",Toast.LENGTH_SHORT).show()
Snackbar.make(it, "谢谢你的喜欢~", Snackbar.LENGTH_SHORT)
.setAction("你是有点东西的") {
Toast.makeText(this, "啊对", Toast.LENGTH_SHORT).show()
}
.show()
}
}
...
}

(OS:下面这个图没换)

1233

12.3.3 CoordinatorLayout(协调布局)

刚才辣个,Snakebar 遮住了我们的FAB,怎么处理。

CoordinatorLayout可以说是一个加强版的FrameLayout,事实上,CoordinatorLayout 可以监听其所有子控件的各种事件,并自动帮助我们做出最为合理的响应。举个简单的例子,刚才弹出的 Snackbar 提示将悬浮按钮遮挡住了,而如果我们能让 CoordinatorLayout 监听到 Snackbar 的弹出事件,那么它会自动将内部的 FloatingActionButton 向上偏移,从而确保不会被Snackbar遮挡。

至于CoordinatorLayout的使用:将原来的FrameLayout替换一下就可以了。修改activity_main.xml中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout 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:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/design_default_color_primary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16sp"
android:src="@drawable/good"
app:elevation="8dp"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

……

</androidx.drawerlayout.widget.DrawerLayout>

Snakbar是FloatingActionButton的儿子,座椅也会被协调布局管着拉。

image-20221023182719470

一个字,丝滑流畅。

12.4 卡片式布局

12.4.1 MaterialCardView

MaterialCardView也是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉。

1
2
3
4
5
6
7
8
9
10
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="4dp"
app:elevation="5dp">
<TextView
android:id="@+id/infoText"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.card.MaterialCardView>

我们准备在屏幕中间放一些水果,要用RecyclerView

首先来加一个依赖:app/build.gradle文件

1
2
3
4
5
dependencies {
...
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'com.github.bumptech.glide:glide:4.14.2'
}

上述声明的第二行是添加了Glide库的依赖。Glide是一个图片加载库

修改activity_main.xml中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout 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:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.coordinatorlayout.widget.CoordinatorLayout
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"/>

……

</androidx.coordinatorlayout.widget.CoordinatorLayout>

……

</androidx.drawerlayout.widget.DrawerLayout>

写个实体类Fruit

1
2
3
package com.lmc.android121

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

为RecyclerView的子项指定一个我们自定义的布局,在layout目录下新建
fruit_item.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
app:cardCornerRadius="4dp"
>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/fruitImage"
android:layout_width="match_parent"
android:layout_height="100dp"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="5dp"
android:textSize="16sp"/>
</LinearLayout>

</com.google.android.material.card.MaterialCardView>

这里使用了MaterialCardView来作为子项的最外层布局,从而使得RecyclerView中的每个元素都是在卡片当中的。由于MaterialCardView是一个FrameLayout,因此它没有什么方便的定位方式,这里我们只好在MaterialCardView中再嵌套一个LinearLayout,然后在LinearLayout中放置具体的内容。

scaleType属性,这个属性可以指定图片的缩放模式.centerCrop模式,它可以让图片保持原有比例填充满ImageView,并将超出屏幕的部分裁剪掉。


为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FruitAdapter(val context: Context, val fruitList: List<Fruit>) :
RecyclerView.Adapter<FruitAdapter.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 {www.blogss.cn
val view = LayoutInflater.from(context).inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitName.text = fruit.name
Glide.with(context).load(fruit.imageId).into(holder.fruitImage)
}
override fun getItemCount() = fruitList.size
}

首先调用Glide.with()方法并传入一个Context、Activity或Fragment参数,然后调用load()方法加载图片,可以是一个URL地址,也可以是一个本地路径,或者是一个资源id,最后调用into()方法将图片设置到具体某一个ImageView中就可以了。


最后修改MainActivity中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class MainActivity : AppCompatActivity() {
val fruits = mutableListOf<Fruit>(
Fruit("双子", R.drawable.sz),
Fruit("双鱼", R.drawable.sy),
Fruit("处女", R.drawable.cn),
Fruit("天秤", R.drawable.tc),
Fruit("射手", R.drawable.ss),
Fruit("巨蟹", R.drawable.jx),
Fruit("摩羯", R.drawable.mx),
Fruit("水瓶", R.drawable.sp),
Fruit("狮子", R.drawable.shizi),
Fruit("白羊", R.drawable.by),
Fruit("金牛", R.drawable.jn),
Fruit("天蝎", R.drawable.tx)
)
val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
initFruits()
val layoutManager = GridLayoutManager(this, 2)
recyclerView.layoutManager = layoutManager
val adapter = FruitAdapter(this, fruitList)
recyclerView.adapter = adapter
}
private fun initFruits() {
fruitList.clear()
repeat(50) {
val index = (0 until fruits.size).random()
fruitList.add(fruits[index])
}
}
...
}

image-20221021225157779

我们的Toolbar怎么不见了!仔细观察一下原来是被RecyclerView给挡住了

12.4.2 AppBarLayout

为啥子被遮住了:RecyclerView和Toolbar都是放置在CoordinatorLayout中的,而前面已经说过,CoordinatorLayout就是一个加强版的FrameLayout,那么FrameLayout中的所有控件在不进行明确定位的情况下,默认都会摆放在布局的左上角,从而产生了遮挡的现象。

AppBarLayout。AppBarLayout实际上是一个垂直方向的LinearLayout

第一步将Toolbar嵌套到AppBarLayout中,第二步给RecyclerView指定一个布局行为。修改activity_main.xml中的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>

当AppBarLayout接收到滚动事件的时候,它内部的子控件其实是可以指定如何去响应这些事件的,通过app:layout_scrollFlags属性就能实现。修改activity_main.xml中的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_scrollFlags="scroll|enterAlways|snap" />
</com.google.android.material.appbar.AppBarLayout>
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>

就加了那一行拉

这里在Toolbar中添加了一个app:layout_scrollFlags属性,并将这个属性的值指定成了scroll|enterAlways|snap。其中,scroll表示当RecyclerView向上滚动的时候,Toolbar会跟着一起向上滚动并实现隐藏;enterAlways表示当RecyclerView向下滚动的时候,Toolbar会跟着一起向下滚动并重新显示;snap表示当Toolbar还没有完全隐藏或显示的时候,会根据当前滚动的距离,自动选择是隐藏还是显示。

向下拉就会隐藏。上拉就会显示顶部的辣个

120401

12.5 下拉刷新

SwipeRefreshLayout就是用于实现下拉刷新功能的核心类,我们把想要实现下拉刷新功能的控件放置到SwipeRefreshLayout中,就可以迅速让这个控件支持下拉刷新。

添加插件:

1
2
3
4
dependencies {
...
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
}

修改 activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout 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:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
……
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
……
</androidx.coordinatorlayout.widget.CoordinatorLayout>
……
</androidx.drawerlayout.widget.DrawerLayout>

我们在 RecyclerView 的外面又嵌套了一层 SwipeRefreshLayout,这样
RecyclerView 就自动拥有下拉刷新功能了。另外需要注意,由于 RecyclerView 现在变成了 SwipeRefreshLayout 的子控件,因此之前使用 app:layout_behavior 声明的布局行为现在也要移到 SwipeRefreshLayout 中才行

在代码中处理具体的刷新逻辑才行。修改MainActivity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
swipeRefresh.setColorSchemeResources(R.color.colorPrimary)
swipeRefresh.setOnRefreshListener {
refreshFruits(adapter)
}
}
private fun refreshFruits(adapter: FruitAdapter) {
thread {
Thread.sleep(2000)
runOnUiThread {
initFruits()
adapter.notifyDataSetChanged()
swipeRefresh.isRefreshing = false
}
}
}
...
}

image-20221022115433792

12.6 可折叠式标题栏

12.6.1 CollapsingToolbarLayout

一个作用于Toolbar基础之上的布局

干啥的?让Toolbar更加强大

注意事项:

  1. CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为AppBarLayout的直接子布局来使用。而AppBarLayout又必须是CoordinatorLayout的子布局
12.6.1.1 具体显示的界面布局

一个额外的Activity作为水果的详情展示界面

→New→Activity→Empty Activity,创建一个FruitActivity,并将布局名指定成 activity_fruit.xml ,然后我们开始编写水果详情展示界面的布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">www.blogss.cn
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:id="@+id/fruitImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

app:layout_collapseMode比较陌生。它用于指定当前控件在CollapsingToolbarLayout折叠过程中的折叠模式,其中Toolbar指定成pin,表示在折叠的过程中位置始终保持不变,ImageView指定成parallax,表示会在折叠的过程中产生一定的错位偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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=".FruitActivity"
android:fitsSystemWindows="true"
>

<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp"
android:fitsSystemWindows="true">

<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/design_default_color_primary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:fitsSystemWindows="true">

<ImageView
android:id="@+id/fruitImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
android:fitsSystemWindows="true"/>

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />

</com.google.android.material.appbar.CollapsingToolbarLayout>

</com.google.android.material.appbar.AppBarLayout>

<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="35dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp"
app:cardCornerRadius="4dp">

<TextView
android:id="@+id/fruitContentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp" />
</com.google.android.material.card.MaterialCardView>


</LinearLayout>
</androidx.core.widget.NestedScrollView>

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_comment"
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>
12.6.2.1 逻辑代码

修改FruitActivity中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class FruitActivity : AppCompatActivity() {
companion object {
const val FRUIT_NAME = "fruit_name"
const val FRUIT_IMAGE_ID = "fruit_image_id"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_fruit)
val fruitName = intent.getStringExtra(FRUIT_NAME) ?: ""
val fruitImageId = intent.getIntExtra(FRUIT_IMAGE_ID, 0)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
collapsingToolbar.title = fruitName
Glide.with(this).load(fruitImageId).into(fruitImageView)
fruitContentText.text = generateFruitContent(fruitName)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
finish()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun generateFruitContent(fruitName: String) = fruitName.repeat(500)
}

修改FruitAdapter中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FruitAdapter(val context: Context, val fruitList: List<Fruit>) :
RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.fruit_item, parent, false)
val holder = ViewHolder(view)
holder.itemView.setOnClickListener {
val position = holder.adapterPosition
val fruit = fruitList[position]
val intent = Intent(context, FruitActivity::class.java).apply {
putExtra(FruitActivity.FRUIT_NAME, fruit.name)
putExtra(FruitActivity.FRUIT_IMAGE_ID, fruit.imageId)
}
context.startActivity(intent)
}
return holder
}
...
}

12.6.2 充分利用系统状态栏空间

给你想要在系统状态栏显示的控件以及父类控件全部就加上android:fitsSystemWindows属性指定成true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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=".FruitActivity"
android:fitsSystemWindows="true"
>

<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp"
android:fitsSystemWindows="true">

<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/design_default_color_primary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:fitsSystemWindows="true">

<ImageView
android:id="@+id/fruitImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
android:fitsSystemWindows="true"/>

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />

</com.google.android.material.appbar.CollapsingToolbarLayout>

</com.google.android.material.appbar.AppBarLayout>

<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="35dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp"
app:cardCornerRadius="4dp">

<TextView
android:id="@+id/fruitContentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp" />
</com.google.android.material.card.MaterialCardView>


</LinearLayout>
</androidx.core.widget.NestedScrollView>

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_comment"
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

然后把系统状态栏改为透明:

1
2
3
4
5
6
7
8
9
10
11
12
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="FruitActivityTheme" parent="AppTheme">
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>

然后去AndroidManifest.xml中,让FruitActivity用这个主题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.materialtest">
<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=".FruitActivity"
android:theme="@style/FruitActivityTheme">
</activity>
</application>
</manifest>

十三,JetPack

开发组件工具箱,帮助写出更加简洁的代码,并简化开发过程。

有很好的兼容性。

我们这个章节主要研究基础架构组件。

分类图:

image-20221029175701071

13.1 ViewModel

发现的问题:Activity要负责处理逻辑,控制UI,处理网络请求……任务太重

ViewModel:分担Activity一部分工作,==专门放和界面相关数据==

界面上能看到的数据,相关变量都放ViewModel中。因为屏幕旋转时Activity重新创建,数据会丢失,但是ViewModel不会。

ViewModel生命周期:

image-20220729152435760

13.1.1 基本用法

添加依赖

1
2
3
4
dependencies {
...
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
}

编程规范:给每一个 Activity 和 Fragment 都创建一个对应的 ViewModel

  1. 新建一个MainViewModel

    1
    2
    3
    4
    5
    6
    7
    package com.lmc.android1301

    import androidx.lifecycle.ViewModel
    //所有与界面相关的数据都放在ViewModel中
    class MainViewModel:ViewModel() {
    var counter = 0
    }
  2. 改xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
    android:id="@+id/infoText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:textSize="32sp" />
    <Button
    android:id="@+id/plusOneBtn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="加一"/>

    </LinearLayout>
  3. MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.lmc.android1301

    import android.os.Bundle
    import android.widget.Button
    import android.widget.TextView
    import androidx.appcompat.app.AppCompatActivity
    import androidx.lifecycle.ViewModelProvider

    class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
    findViewById<Button>(R.id.plusOneBtn).setOnClickListener {
    viewModel.counter++
    refreshCounter()
    }
    refreshCounter()
    }

    private fun refreshCounter() {
    findViewById<TextView>(R.id.infoText).text = viewModel.counter.toString()
    }
    }

    解释的点:

    1. 不能直接在onCreate里面创建ViewModel的实例,因为这样他就随着Activity的创建销毁而变化了。我们一般通过ViewModelProvider来获取。下面是语法:

      ViewModelProvider(<你的Activity或Fragment实例>).get(<你的ViewModel>::class.java)

结果:image-20221029183437757

横过来也没有丢失数据:

image-20221029183650026

13.1.2 向 ViewModel 传递参数

小优化:推出程序重新打开数据不丢失

  1. MainViewModel

    1
    2
    3
    4
    5
    6
    7
    8
    package com.lmc.android1301

    import androidx.lifecycle.ViewModel

    //所有与界面相关的数据都放在ViewModel中
    class MainViewModel(countReserved: Int) : ViewModel() {
    var counter = countReserved
    }
  2. 新建一个MainViewModelFactory类,实现ViewModelProvider.Factory接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.lmc.android1301

    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.ViewModelProvider

    class MainViewModelFactory(private val countReserved:Int):ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
    return MainViewModel(countReserved) as T
    //这里可以创建MainViewModel实例,因为这里的create方法执行实际和Activity的生命周期无关
    }
    }
  3. 一个清零按钮

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    ……

    <Button
    android:id="@+id/clearBtn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="清零"/>

    </LinearLayout>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    package com.lmc.android1301

    import android.content.Context
    import android.content.SharedPreferences
    import android.os.Bundle
    import android.widget.Button
    import android.widget.TextView
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.content.edit
    import androidx.lifecycle.ViewModelProvider

    class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainViewModel
    lateinit var sp:SharedPreferences


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

    sp = getPreferences(Context.MODE_PRIVATE)
    val countReserved = sp.getInt("count_reserved",0)

    viewModel = ViewModelProvider(this,MainViewModelFactory(countReserved))
    .get(MainViewModel::class.java)
    ……
    findViewById<Button>(R.id.clearBtn).setOnClickListener {
    viewModel.counter = 0
    refreshCounter()
    }
    refreshCounter()
    }

    override fun onPause() {
    super.onPause()
    sp.edit {
    putInt("count_reserved",viewModel.counter)
    }
    }
    ……
    }

没问题了,现在哪怕中途退出,数据也会保存好。

13.2 Lifecycles

让任何一个类都能轻松感知到Activity的生命周期,同时又不需要在Activity中编写大量的逻辑处理。

  1. 新建一个MyObserver

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.lmc.android1301

    import android.util.Log
    import androidx.lifecycle.Lifecycle
    import androidx.lifecycle.LifecycleObserver
    import androidx.lifecycle.OnLifecycleEvent

    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")
    }
    }
  2. 怎么让MyObserver 感知到Activity的生命周期变化

    1
    lifecycleOwner.lifecycle.addObserver(MyObserver())

    然后我们的Activity和Fragment都是lifecycleOwner

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    ...
    lifecycle.addObserver(MyObserver())//加一句这个就可以了
    }
    ...
    }
  3. 主动获知当前的生命周期状态

    1
    2
    3
    4
    5
    6
    7
    class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver {
    ...
    }

    lifecycle.addObserver(MyObserver(this.lifecycle))
    lifecycle.currentState//获取当前生命周期的状态
    //这个地方有坑,注意看视频演示。

    image-20221029214016925

13.3 LiveDate

LiveData是Jetpack提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。和ViewModel很贴近。

出现的问题:我们刚刚写的那个计数器的功能只能在单线程中工作,ru过ViewMode中开了新的线程去执行耗时任务,那我们点击加一按钮之后就得到的还是之前的数据;

解决办法:将计数器的计数用LiveData包装,然后在Activity中去观察它,这样就可以主动的将数据变化通知给Activity

13.3.1 基本用法

  1. MainViewModel :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.lmc.android1301

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

//所有与界面相关的数据都放在ViewModel中
class MainViewModel(countReserved: Int) : ViewModel() {
val counter = MutableLiveData<Int>()//一种可变的LiveData
//getValue()、setValue()和postValue()最后一个是用在非主线程中给LiveData设置数据的

init {
counter.value = countReserved
}

fun plusOne() {
val count = counter.value ?: 0
counter.value = count + 1
}

fun clear() {
counter.value = 0
}
}
  1. MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
    plusOneBtn.setOnClickListener {
    viewModel.plusOne()
    }
    clearBtn.setOnClickListener {
    viewModel.clear()
    }
    viewModel.counter.observe(this){ count ->
    findViewById<TextView>(R.id.infoText).text = count.toString()
    }
    }
    override fun onPause() {
    super.onPause()
    sp.edit {
    putInt("count_reserved", viewModel.counter.value ?: 0)
    }
    }
    }
  2. 优化:

    counter 不能暴露给外面。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package com.lmc.android1301

    import androidx.lifecycle.LiveData
    import androidx.lifecycle.MutableLiveData
    import androidx.lifecycle.ViewModel

    //所有与界面相关的数据都放在ViewModel中
    class MainViewModel(countReserved: Int) : ViewModel() {

    val counter:LiveData<Int>
    get() = _counter

    private val _counter = MutableLiveData<Int>()//一种可变的LiveData
    //getValue()、setValue()和postValue()最后一个是用在非主线程中给LiveData设置数据的

    init {
    _counter.value = countReserved
    }

    fun plusOne() {
    val count = counter.value ?: 0
    _counter.value = count + 1
    }

    fun clear() {
    _counter.value = 0
    }
    }

13.3.2 map/switchMap

LiveData的两种转换方法

13.3.2.1 map

将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//1. 新建也给User类

package com.lmc.android1301

data class User(var firstName: String, var lastName: String, var age: Int)



//2. MainViewModel
class MainViewModel(countReserved: Int) : ViewModel() {
//创建一个LiveData来包含User类型数据
val userLiveData = MutableLiveData<User>()
...
}



//3.现在想只能看到LiveData的名字,不能看到age
class MainViewModel(countReserved: Int) : ViewModel() {
//创建一个LiveData来包含User类型数据
private val userLiveData = MutableLiveData<User>()
val userName:LiveData<String> = Transformations.map(userLiveData) {
"${it.firstName} ${it.lastName}"
}
...
}

外面使用的时候只要检擦userName这个LiveData就可以了,

当userLiveData的数据发生变化,map方法就会监听到变化并执行转换函数中的代码,然后把处理好的数据通知给userName的观察者

13.3.2.2 switchMap

如果ViewModel中的某个LiveData对象是调用另外的方法获取的,那么我们就可以借助
switchMap()方法,将这个LiveData对象转换成另外一个可观察的LiveData对象。

  1. 新建一个 Repository 单例类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.lmc.android1301

    import androidx.lifecycle.LiveData
    import androidx.lifecycle.MutableLiveData

    object Repository {
    //getUser方法返回一个包含User数据的LiveData对象,且每次调用该方法都会返回一个新的LiveData实例
    fun getUser(userId: String): LiveData<User> {
    val liveData = MutableLiveData<User>()
    liveData.value = User(userId, userId, 0)
    return liveData
    }
    }
  2. MainViewModel

    1
    2
    3
    4
    5
    6
    class MainViewModel(countReserved: Int) : ViewModel() {
    ...
    fun getUser(userId: String): LiveData<User> {
    return Repository.getUser(userId)www.blogss.cn
    }
    }
  3. Activity 中怎么观察 LiveData 中的数据变化?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //这样?
    viewModel.getUser(userId).observe(this) { user ->
    }//getUser每次返回一个新的LiceData但我们一直在观察老的LiveData实例,看不到数据变化。这个时候LiveData是不可观察的。
    //!!!MainViewModel
    class MainViewModel(countReserved: Int) : ViewModel() {
    ...
    private val userIdLiveData = MutableLiveData<String>()
    val user: LiveData<User> = Transformations.switchMap(userIdLiveData) { userId ->
    Repository.getUser(userId)
    }
    fun getUser01(userId:String) {//这个地方不要用getUser
    userIdLiveData.value = userId
    }
    }

    switchMap整体工作流程:

    外部调用MainViewModel的getUser()方法,将传入的userId值设置到userIdLiveData当中。

    一旦userIdLiveData的数据发生变化,那么观察userIdLiveData的switchMap()方法就会执行,并且调用我们编写的转换函数。然后在转换函数中调用Repository.getUser()方法获取真正的用户数据。
    同时,switchMap()方法会将Repository.getUser()方法返回的LiveData对象转换成一个可观察的LiveData对象,对于Activity而言,只要去观察这个LiveData对象就可以了。

  4. 修改activity_main.xml文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    ...
    <Button
    android:id="@+id/getUserBtn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="Get User"/>
    </LinearLayout>
  5. 修改MainActivity 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
findViewById<Button>(R.id.getUserBtn).setOnClickListener {
val userId = (0..10000).random().toString()
viewModel.getUser01(userId)
}
viewModel.user.observe(this){
findViewById<TextView>(R.id.infoText).text = it.firstName
}
...
}

这里image-20221030223037505

  1. ViewModel中某个获取数据的方法有可能是没有参数的,这个时
    候代码应该怎么写呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MyViewModel : ViewModel() {
    private val refreshLiveData = MutableLiveData<Any?>()
    val refreshResult = Transformations.switchMap(refreshLiveData) {
    Repository.refresh() // 假设Repository中已经定义了refresh()方法
    }
    fun refresh() {
    refreshLiveData.value = refreshLiveData.value//触发数据变化事件
    }
    }

一些小细节:

  1. LiveData与ViewModel结合在一起使用时候,底层都是Lifecycles组件作为Activity和ViewModel通信桥梁。
  2. 由于要减少性能消耗,当Activity处于不可见状态的时候(比如手机息屏,或者被其他的Activity遮挡),如果LiveData中的数据发生了变化,是不会通知给观察者的。只有当Activity重新恢复可见状态时,才会将数据通知给观察者,而LiveData之所以能够实现这种细节的优化,依靠的还是Lifecycles组件。
  3. 如果在Activity处于不可见状态的时候,LiveData发生了多次数据变化,当
    Activity恢复可见状态时,只有最新的那份数据才会通知给观察者,前面的数据在这种情况下相当于已经过期了,会被直接丢弃。

13.4 Room

Android官方推出的一个专门操作 SQLite 数据库的ORM

ORM:Object Relational Mapping 对象关系映射 我们用的编程语言是面向对象编程语言,然而数据库确实关系型数据库,他们之间建立一种映射关系就是 ORM。

使用ORM得好处:直接用面向对象得思维去和数据库打交道,对大多数情况下就不用跟SQL语句打交道了,也不用担心操作数据让项目整体代码变得混乱。

13.4.1 使用 Room 进行增删改查

13.4.1.1 Room整体结构
  1. Entity。封装实际数据得实体类。每个实体类都会在数据库中对饮一张表,表中得列就是根据实体类的字段自动生成;
  2. Dao。数据访问对象,在这里对数据库的各项操作进行封装。这样逻辑层就不用和底层数据库打交道直接和Dao层交互;
  3. Database。定义数据库中的关键信息,数据库版本号,包含的实体类以及提供Dao层访问实例。
13.4.1.2 Room使用步骤
  1. 依赖引入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
plugins {
id 'kotlin-kapt'
}

dependencies {
def room_version = "2.3.0"

implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"

// optional - RxJava2 support for Room
implementation "androidx.room:room-rxjava2:$room_version"

// optional - RxJava3 support for Room
implementation "androidx.room:room-rxjava3:$room_version"

// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"

// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}

解释一下:Room会根据我们在项目中声明的注解来动态生成代码,所以要用kapt引入Room的编译时注解库,而启动编译时注解功能要先添加kotlin-kapt插件。(kotlin项目中使用这个,如果是Java项目那就只用 annotationProcessor,书上的用不了,只有这样才能用。)

  1. 改User成Entity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.lmc.android1301

    import androidx.room.Entity
    import androidx.room.PrimaryKey

    @Entity
    data class User(var firstName: String, var lastName: String, var age: Int) {
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
    }

  2. 定义Dao

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    //新建一个UserDao接口
    package com.lmc.android1301

    import androidx.room.*

    @Dao
    interface UserDao {
    @Insert
    fun insertUser(user: User): Long

    @Update
    fun updateUser(newsUser: User)

    @Delete
    fun deleteUser(user: User)

    //从数据库中查询数据
    @Query("select * from User")
    fun loadAllUsers(): List<User>

    @Query("select * from User where age > :age")
    fun loadUsersOlderThan(age: Int): List<User>


    //使用非实体类参数来增删改数据(这时候只能用Query注解)
    @Query("delete from User where lastName = :lastName")
    fun deleteUserByLastName(lastName: String): Int
    }
  3. 定义Database

    新建一个AppDatabase.kt文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package com.lmc.android1301

    import android.content.Context
    import androidx.room.Database
    import androidx.room.Room
    import androidx.room.RoomDatabase

    @Database(version = 1, entities = [User::class])//多个实体类之间用逗号隔开
    abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao//提供相应的抽象方法,用于获取之前编写的Dao的实例
    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
    }
    }
    }
    }
13.4.1.3 Room功能测试
  1. activity_main.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    ...
    <Button
    android:id="@+id/getUserBtn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="Get User"/>
    <Button
    android:id="@+id/addDataBtn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="Add Data"/>
    <Button
    android:id="@+id/updateDataBtn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="Update Data"/>
    <Button
    android:id="@+id/deleteDataBtn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="Delete Data"/>
    <Button
    android:id="@+id/queryDataBtn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="Query Data"/>
    </LinearLayout>
  2. 修改MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?)
    ...
    //Room
    val userDao = AppDatabase.getDatabase(this).userDao()
    val user1 = User("Tom", "Brady", 40)
    val user2 = User("Tom", "Hanks", 63)

    findViewById<Button>(R.id.addDataBtn).setOnClickListener {
    thread {
    user1.id = userDao.insertUser(user1)
    user2.id = userDao.insertUser(user2)
    }
    }
    findViewById<Button>(R.id.updateDataBtn).setOnClickListener {
    thread {
    user1.age = 42
    userDao.updateUser(user1)
    }
    }
    findViewById<Button>(R.id.deleteDataBtn).setOnClickListener {
    thread {
    userDao.deleteUserByLastName("Hanks")
    }
    }
    findViewById<Button>(R.id.queryDataBtn).setOnClickListener {
    thread {
    for (user in userDao.loadAllUsers()) {
    Log.d("MainActivity", user.toString())
    }
    }
    }
    ...
    }

    Room默认是不允许在主线程中进行数据库操作的,所以都放在子线程中。

    非要主线程试试:(构建AppDatabase实例的时候,加入一个allowMainThreadQueries()方法 )

    1
    2
    3
    Room.databaseBuilder(context.applicationContext, AppDatabase::class.java,"app_database")
    .allowMainThreadQueries()
    .build()
  3. 结果:

    添加User后image-20221031162745042

    更新后image-20221031162835122

    执行删除后image-20221031162914866

13.4.2 Room 的数据库升级

Room的数据库升级方面很垃圾,LitePal这个还行有兴趣了解一下。

看看Room最简单的怎么用:

1
2
3
4
//简单粗暴
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java,"app_database")
.fallbackToDestructiveMigration()
.build()

后果:只要数据库升级,Room就将当前的数据库销毁,然后重新创建。

代价:之前数据库中的所有数据都丢失了。

故:这种只能在开发和测试的时候使用。

Room升级数据库的正规写法:

  1. 新增一个Book实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.lmc.android1301

    import androidx.room.Entity
    import androidx.room.PrimaryKey

    @Entity
    data class Book(var name: String, var pages: Int) {
    @PrimaryKey(autoGenerate = true)
    var id:Long = 0
    }

  2. BookDao接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.lmc.android1301

    import androidx.room.Dao
    import androidx.room.Insert
    import androidx.room.Query

    @Dao
    interface BookDao {
    @Insert
    fun insertBook(book: Book):Long
    @Query("select * from Book")
    fun loadAllBooks():List<Book>
    }
  3. 修改AppDatabase,在里面写数据库升级的逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    //新增一张表的写法
    @Database(version = 2, entities = [User::class, Book::class])
    abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun bookDao(): BookDao
    companion object {
    val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("create table Book (id integer primary
    key autoincrement not null, name text not null,
    pages integer not null)")
    }
    }
    private var instance: AppDatabase? = null
    fun getDatabase(context: Context): AppDatabase {
    instance?.let {
    return it
    }
    return Room.databaseBuilder(context.applicationContext,
    AppDatabase::class.java, "app_database")
    .addMigrations(MIGRATION_1_2)
    .build().apply {
    instance = this
    }
    }
    }
    }
  4. 只一个表里面加点东西

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.lmc.android1301

    import androidx.room.Entity
    import androidx.room.PrimaryKey
    //Book表加了作者
    @Entity
    data class Book(var name: String, var pages: Int,var author:String) {
    @PrimaryKey(autoGenerate = true)
    var id:Long = 0
    }

    修改对应AppDatabase中的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    package com.lmc.android1301

    import android.content.Context
    import androidx.room.Database
    import androidx.room.Room
    import androidx.room.RoomDatabase
    import androidx.room.migration.Migration
    import androidx.sqlite.db.SupportSQLiteDatabase


    @Database(version = 3, entities = [User::class, Book::class])
    abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun bookDao(): BookDao

    companion object {
    val MIGRATION_1_2 = object : Migration(1, 2) {
    //版本从1到2就会执行这里的代码
    override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("create table Book (id integer primary key autoincrement not null, name text not null, pages integer not null)")
    }

    }
    val MIGRATION_2_3 = object : Migration(2, 3) {
    //版本从2到3就会执行这里的代码
    override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("alter table Book add column author text not null default 'unknown'")
    }
    }
    private var instance: AppDatabase? = null
    fun getDatabase(context: Context): AppDatabase {
    instance?.let {
    return it
    }
    return Room.databaseBuilder(
    context.applicationContext,
    AppDatabase::class.java, "app_database"
    )
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)//加上这个就要升级啦
    .build().apply {
    instance = this
    }
    }
    }

    }

13.5 WorkManager

一个处理定时任务的工具:

可以保证即使在应用退出甚至手机重启的情
况下,之前注册的任务仍然将会得到执行,因此WorkManager很适合用于执行一些定期和服务
器进行交互的任务,比如周期性地同步数据

13.5.1 基本用法

  1. 添加依赖
1
2
3
4
dependencies {
...
implementation "androidx.work:work-runtime:2.7.1"
}
  1. 做一个有任务逻辑的后台任务;

    新建一个SimpWork类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package com.lmc.android1301

    import android.content.Context
    import android.util.Log
    import androidx.work.Worker
    import androidx.work.WorkerParameters

    class SimpleWorker(context: Context,params:WorkerParameters):Worker(context,params) {
    override fun doWork(): Result {
    //doWork不会运行在主线程之中
    Log.d("SimpleWorker","在SimpleWorker中执行任务")
    return Result.success()//这里除了成功失败之外还有一个Result.retry(),可以结合 WorkRequest.Builder的setBackoffCriteria()方法来重新执行任
    }
    }
  2. 写这个后台任务的运行条件和约束请求,并发起后台任务请求;

    例如:

    1
    2
    3
    4
    5
    //这种是单次执行的
    val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
    //下面这种是周期性执行的
    val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15,
    TimeUnit.MINUTES).build()//但是周期最少也得是15min
  3. 将后台任务请求放在WorkManager的enqueue方法中,系统知道怎么做。

    1
    WorkManager.getInstance(context).enqueue(request)
  4. 测试

    xml文件,加个按钮。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    ...
    <Button
    android:id="@+id/doWorkBtn"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="Do Work"/>
    </LinearLayout>

    改MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
    doWorkBtn.setOnClickListener {
    val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
    WorkManager.getInstance(this).enqueue(request)
    }
    }
    ...
    }

    image-20221031191601670

13.5.2 处理复杂任务

  1. 让后台任务在指定的延迟时间后运行setInitialDelay()

    第二步写后台请求时:

    1
    2
    3
    val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
    .setInitialDelay(5, TimeUnit.MINUTES)
    .build()
  2. 给后台任务+标签

    1
    2
    3
    4
    val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
    ...
    .addTag("simple")
    .build()

    加标签的好处:可以通过标签来取消后台任务请求。

    1
    2
    3
    4
    5
    6
    7
    8
    WorkManager.getInstance(this).cancelAllWorkByTag("simple")
    //否则就只能用id来取消
    WorkManager.getInstance(this).cancelAllWorkById(request.id)
    //用id只能取消单个,而标签能取消一个系列


    //发疯了取消所有后台任务请求
    WorkManager.getInstance(this).cancelAllWork()
  3. 重新执行任务

    如果后台任务的doWork()方法中返回了Result.retry(),
    那么是可以结合setBackoffCriteria()方法来重新执行任务的

    1
    2
    3
    4
    val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
    ...
    .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
    .build()

    备注:

    1. 后面两个指定多久之后重新执行,最低10s

    2. 第一个参数则用于指定如果任务再次执行失败,下次重试的时间应该以什么样的形式延迟。

      LINEAR:下次重试时间以线性方式延迟;

      EXPONENTIAL:下次延迟时间以指数方式延迟

  4. 监听后台任务执行结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    WorkManager.getInstance(this)
    .getWorkInfoByIdLiveData(request.id)//这里会返回一个LiveData对象
    .observe(this) { workInfo ->
    if (workInfo.state == WorkInfo.State.SUCCEEDED) {
    Log.d("MainActivity", "do work succeeded")
    } else if (workInfo.state == WorkInfo.State.FAILED) {
    Log.d("MainActivity", "do work failed")
    }
    }
    //还可以换成getWorkInfosByTagLiveData()方法,监听同一标签名下所有后台任务请求的运行结果
  5. 链式任务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    val sync = ...//同步后台任务请求
    val compress = ...//压缩
    val upload = ...//上传
    WorkManager.getInstance(this)
    .beginWith(sync)//开启一个链式任务
    .then(compress)
    .then(upload)
    .enqueue()
    //规则:在前面一个任务执行完毕之后才能执行后面一个

总结:WorkManager在国产手机上不稳定,很容易被杀。别拿去实现什么核心功能。

十四,高级技巧

14.1 全局获取 Context 的技巧

实现方法:定义一个自己的Application类,

这个类每次app启动,系统就会自动对这个类进行初始化。它可以用来便于管理app内一些全局变量的状态信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.lmc.android1401

import android.annotation.SuppressLint
import android.app.Application
import android.content.Context

class MyApplication:Application() {
companion object {
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
}

override fun onCreate() {
super.onCreate()
context = applicationContext
}
}

注解作用:这里本来Context是不能设置为静态变量的,可能会导致内存泄漏。但是我们这里的Context是直接从application中拿出来的,不是Activity不是Service全局只会有这么一个实例,而且整个app的生命周期内都不会回收,所以直接给一个注解说明一下就可以了。

告诉系统以后初始化我们自己做的这个application类,别整自己那套:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.lmc.android1401">

<application
android:name=".MyApplication"
……
</application>

</manifest>

就是那个 name

中了,以后不管你在项目的哪里,想要context直接MyApplication.context 就阔以了。

14.2 使用 Intent 传递对象

问题:

Intent在传递信息的时候啊,只能规规矩矩地传那些基本的数据类型,比如什么Int,String……啥的,我想传个对象,比如Student怎么整?

两个办法:Serializable和Parcelable

14.2.1 Serializable 方式

关键三字:序列化。

也就是说我们把这个对象序列化了,然后序列后的东西就可以随便传输了。(JavaSE IO有说过)

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Person实体中
class Person : Serializable {//对象能序列化的前提就是实体类搞这个接口
var name = ""
var age = 0
}
//FirstActivity中 发送?
val person = Person()
person.name = "Tom"
person.age = 20
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("person_data", person)
startActivity(intent)
//SecondActivity中 接受
val person = intent.getSerializableExtra("person_data") as Person

有个小问题:就是FirstActivity中序列化前的Person和SecondActivity反序列化出来的Person是同一个对象嘛?

不是,他们是两个不同的对象只是里面的数据相同。HashCode不一样但是value一样。

这种办法因为要将整个数据序列化,所以效率会低一点。

14.2.2 Parcelable 方式

原理:把对象一块块的分解成支持传送的数据类型然后再传。

一般形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//Person实体类中
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)
}
}
}
//FirstActivity中
val person = intent.getParcelableExtra("person_data") as Person

太麻烦了,看下面这个:

1
2
3
//Person实体类
@Parcelize
class Person(var name: String, var age: Int) : Parcelable

前提:要传输的所有数据都放在主构造函数里面。

推荐用这种方法

14.3 定制自己的日志工具

安卓自己的日志工具很强,但是还有弱点:比如日志打印的控制。

开发阶段许多日志需要打印,上线之后就把日志屏蔽。(为什么?机密泄露,性能降低)

解决办法:定制一个自己的日志工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//新建一个LogUtil单例类
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)
}
}
}
//这样我们设置level的值为最低就可以看到所有的信息,设置为最高就只能看到erro信息。对应的平常做VERBOSE,上线就改ERROR。

14.4 调试 Android 程序

  1. 添加断点。鼠标左击一下,取消就再加一下;

  2. 开启调试。小虫子点一下;

    image-20221101160000897

  3. 其实这一个用的更多:image-20221101160617047

    比如你做了一个登录界面,用前面的那个,如果你的断点打在靠后的位置,那么前面输入数据的步骤就一卡一卡的。这时候我们就可以用后面这款。

    打好断点,然后正常启动,然后输入数据后点击那个奇怪的小虫子,再去点登录。这时候又可以直接debug。

14.5 深色主题

Android系统官方支持夜间模式:深色主题是在Android10.0。

也就是说,之前只能app自己打开关闭深色模式,现在Android系统打开深色模式全部适配了这个功能的APP就都切换成深色模式了。方便太多。

OS:要是用户系统开个深色模式,别的APP都切换成黑的了,救你的APP贼拉刺眼那样很败兴致。

其实吧,现在AS都是自动给你搞了深色模式。不信之前我们写的小demo你手机系统切换到深色模式回去看看?是不是都变黑了。

14.5.1 Force Dark(了解)

原理:系统会分析你的App的每一层View并把他们的颜色自动换成和深色主题更加贴合的颜色。(注意只有原App是浅色才会这样,本来就是深色那还玩个锤子)

  1. 做个values-v29目录,然后写个styles.xml文件,代码下面。(为啥29?Android10的API就是29)

    1
    2
    3
    4
    5
    6
    7
    8
    <resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>www.blogss.cn
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    <item name="android:forceDarkAllowed">true</item>
    </style>
    </resources>
  2. 真是又丑又难看哈哈哈哈哈

14.5.2 DayNight主题

这种就是他会根据系统主题来自动变light还是dark。(AS默认自动的就是原生设计中的这种)

如果是老版本,改唯一的style文件为这样:

1
2
3
4
5
6
7
8
9
10
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
...
</resources>

虽说还是有点丑,起码好了点。但是吧这样一个DayLight主题,那个头上还是light的没有一整个dark。(这是一个主题文件夹瘸腿得地方)

为啥?因为我们状态栏是直接指定是啥颜色的,这叫硬编码,你不上手改,系统大人也不能擅做主张啊。

==解决方案:==新建一个values-night目录,然后做一个同名theme文件,里面还是用的DayLight主题,但是标题栏就根据不同情况自己指定好了。 (其实这才是AS默认得那个)

小建议:我们要尽量避免给定颜色这种硬编码方式,而去尝试能够根据当前主题自动切换颜色得主题属性。比如下图:

1
2
3
4
5
6
7
8
9
10
11
12
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Hello world"
android:textSize="40sp"
android:textColor="?android:attr/textColorPrimary" />
</FrameLayout>

想根据浅色主题和深色主题来分别执行不同业务逻辑?

怎么判断当前系统是否为深色模式:

1
2
3
4
5
fun isDarkTheme(context: Context): Boolean {
val flag = context.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK
return flag == Configuration.UI_MODE_NIGHT_YES
}

备注:Kotin中取消了按位运算符,直接换成了英语关键字。(C,爱了。天下苦位运算符久矣。Kotlin揭竿而起!)

and 就是Java的 &

or 就是Java的 |

xor 就是Java的 ^

补充

1. 怎么查看类的继承关系和组成

查看类的继承关系:点类名,然后 Ctrl+H

image-20220713153909268

查看类的组成结构:

image-20220713154314897

image-20220713154428707

2.Kotlin 跟 Java 的关系

xx.kt -> xx.class -> JVM -> 二进制文件

3. Java 是什么语言

编程语言大致可以分为两类:
编译型语言和解释型语言。编译型语言的特点是编译器会将我们编写的源代码一次性地编译成计算机可识别的二进制文件,然后计算机直接执行,像C和C++都属于编译型语言。解释型语言则完全不一样,它有一个解释器,在程序运行时,解释器会一行行地读取我们编写的源代码,然后实时地将这些源代码解释成计算机可识别的二进制数据后再执行,因此解释型语言通常效率会差一些,像Python和JavaScript都属于解释型语言。

  1. Java是解释性语言

4. 为什么 Koltin 会设计出 val 关键字?

  1. 建议定义变量,类,方法,一开始都加上 final

  2. 如果一个类不是专门为继承而设计的,那么就应
    该主动将它加上final声明

    这里你可能会产生疑惑:既然val关键字有这么多的束缚,为什么还要用这个关键字呢?干脆全部用var关键字不就好了。其实Kotlin之所以这样设计,是为了解决 Java 中final关键字没有被合理使用的问题。

    在 Java 中,除非你主动在变量前声明了 final 关键字,否则这个变量就是可变的。然而这并不是一件好事,当项目变得越来越复杂,参与开发的人越来越多时,你永远不知道一个可变的变量会在什么时候被谁给修改了,即使它原本不应该被修改,这就经常会导致出现一些很难排查的问题。因此,一个好的编程习惯是,除非一个变量明确允许被修改,否则都应该给它加上final关键字。

    但是,不是每个人都能养成这种良好的编程习惯。我相信至少有90%的Java程序员没有主动在变量前加上final关键字的意识,仅仅因为Java对此是不强制的。因此,Kotlin 在设计的时候就采用了和Java完全不同的方式,提供了val和var这两个关键字,必须由开发者主动声明该变量是可变的还是不可变的。

    那么我们应该什么时候使用val,什么时候使用var呢?这里我告诉你一个小诀窍,就是永远优先使用val来声明一个变量,而当val没有办法满足你的需求时再使用var。这样设计出来的程序会更加健壮,也更加符合高质量的编码规范。