【Android】基础笔记合集
Android备课笔记
谷歌
一,Android 入门介绍
1.1 了解 Android
1.1.1 Android 系统架构
四层架构,五块区域
- Linux 内核层 -> 驱动
- 系统运行库层 -> C/C++ 做的特性 & Android 运行时库(核心库/Dalvik虚拟机-> Android5 变成了ART)
- 应用框架层 -> API
- 应用层
1.1.2 Android 应用开发特色
-
四大组件
Android系统四大组件分别是活动(Activity)、服务(Service)、广播接收器(Broadcast Receiver)和内容提供器(Content Provider)
-
系统控件
-
SQLite 数据库
轻量级、运算速度极快的嵌入式关系型数据库,支持标准的 SQL 语法,还可以通过 Android 封装好的 API 进行操作,让存储和读取数据变得非常方便。
-
其他
地理位置定位 LBS(Location based server)多媒体 传感器
1.2 搭建环境
1.2.1 开发工具
- Android SDK
- Android Studio
1.2.2 搭建开发环境
-
下载最新版本的 Androi Studio (其实i)
[官网](Download Android Studio & App Tools - Android Developers (google.cn))
-
具体步骤百度一下
1.3,初识 Android Studio
IDE:Android Studio 2021.2.1
1.3.1 建立第一个项目
下面这张图有个注意的点:Name
首字母记得最好大写,OK 了记得 Finish
1.3.2 结构介绍
1.3.2.1 project 结构介绍
-
首先还原到本来目录
-
顶部工具部分介绍
-
project 目录介绍
1.3.2.2 app目录下的结构
1.3.2.3 res 目录下的结构
怎么引用 res 中的资源
写快了,是
string
不是大写的String
,大家自己改一下
这里的 string 可以换成其他的,比如图片换成 drawable ,布局换成 layout
1.3.2.4 build.gradle 详解
外面那个
里面那个
-
plugin
-
android
1.4 第一个 Android 程序解读
1.4.1 AndroidMainfest.xml 解读
这里也可以验证上文说的怎么访问 res 中的资源是不是都是用的 @xxx/xxx
1.4.2 MainActivity 解读
这里的类的继承结构如下
1.4.3 layout 中的 main_activity 界面
1.5 日志
1.5.1 介绍
Android 日志工具类 Log(android.util.Log)
1.5.1.1 Logcat
1.5.1.2 日志级别
下面级别依次递增
- Log.v() verboser
- Log.d() debug
- Log.i() info
- Log.w() warn
- Log.e() erro
演示:
1.5.1.3 Log 过滤器
-
级别
-
自己做一个过滤器
1.5.2 为什么用 Log 不用 println
println 除了方便打出来没有啥优点,去看看鱼皮有个 bug 分享,这个 println 的危害,里面可见一斑。
然而 Log 的级别控制,过滤器,关键字输入框 虽然不是完美但是已经很强大了。
二,Kotlin
2.0 Kotlin 认识篇
Kotlin 的历史:
-
JetBrains 2011 Kotlin 出生 2016 1.0 正式成熟
-
开头 9 年都是用 Java 开发 Android 。Android 1.5 引入NDK支持 C/C++ 本地化开发
-
2017 Google I/O 宣布 Kotlin 开发 Android 地位和 Java 一样
-
2019 Google I/O 宣布 Kotlin First 强推用 Kotlin 开发Android
Kotlin 现状:
国外:Kotlin 很火 Google pay 应用商店排名前 1000 ,60%+用的 Kotlin 开发。 Android官网文档的代码已优先显示Kotlin版本,官方的视频教程以及Google的一些开源项目,也改用了 Kotlin 来实现。
国内: 新兴,保守,过于依赖 Java ,加上敏捷开发,跨端框架,Kotlin 在发展。但坑定是未来版本之王。
Kotlin 的优点:
- 和 Java 100% 兼容 直接调用 Java 代码,使用 Java 第三方库
- 简洁 相对Java 代码量50%-
- 高级 现代高级语法特性
- 安全 NPE 空指针
在哪写 Kotlin?
-
用 IDEA 前提是你安装了 这个
-
在线官方运行网站:https://try.kotlinlang.org (非科学上网访问会有点慢)
-
Android Studio 刚好上节课安装了这个我们就用这个来写
为啥 Android 是 Google 开发的系统,Kotlin 确是 JetBrains 开发出来的?
编程语言分类 -> Java 语言机制 -> Kotlin语言机制
那么如果我开发了一门新的编程语言,然后自己做了个编译器,让它将这门新语言的代码编译成同样规格的class文件,Java 虚拟机能不能识别呢?没错,这其实就是Kotlin的工作原理了
2.1 变量
val:value 给了初始值不能改变 (通常情况下,先用这个)
var:variable 值可以改变
类型推导机制(变量延迟赋值就得显示声明)
全是对象数据类型
为什么会有 val?
2.2 函数
function(Koltin) Method
什么是函数? 藏宝图
函数结构解释
自动代码补全 懒?手撕代码?不不不 引包
简化的语法糖(简洁优雅的甜头)演示一步步
1 | //自己定义的函数(返回两个数中大的那个) |
注意:Kotlin函数参数是不可变的,也就是说固定val
2.3 逻辑控制
程序的执行语句主要分为3种:顺序语句、条件语句和循环语句。
2.3.1 选择
2.3.1.1 if
1 | //3.用 if 实现(最传统的办法) |
2.3.1.2 when
1 | fun main() { |
2.3.2 循环
2.3.2.1 for - in
可以用来遍历区间 数组和集合
1 | //1,区间定义 |
2.4 面向对象编程
啥叫面向对象啊?
面向对象和面向过程的区别就是面向对象是有类的。这个类是一种概括性的,笼统性的描绘和共性封装,一般是名词。比如一个类是 人。然后类中有属性,函数。属性:比如名字身高,一般是名词。函数相当于人的行为,比如吃喝拉撒。然后用这个类,按照这个类创造的对象就是把类中这些东西填满的具体的人,比如人类型变成人对象的过程就是造小人。
2.4.1 类与对象
1 | fun main(){ |
这就是面向对象编程最基本的用法了,简单概括一下,就是要先将事物封装成具体的类,然后将事物所拥有的属性和能力分别定义成类中的字段和函数,接下来对类进行实例化,再根据具体的编程需求调用类中的字段和方法即可。
—— 《第一行代码》
2.4.2 继承与构造函数 ⭐
继承
为什么有继承?继承思想是什么?
人和学生老师的故事
1 | fun main(){ |
构造函数
什么是构造函数? 类实例化的工具 出厂就有的一些设置 就命名
Kotlin 中 两种:主构造函数 次构造函数(几乎很少用,被替代性很高)
主构造函数
-
特点:没有函数体
-
如果没写就是每个类默认都有一个不带参数的主构造函数 也称无参构造函数
-
当然你也可以显示的给它指明参数
-
代码演示:
1
2
3
4class Student(val sno: String, val grade: Int) : Person() {
}//小思考:为啥是 val 鸭?
val student = Student("a123", 5) -
如果想在主构造函数写点逻辑:
init 结构体:
1
2
3
4
5
6class Student(val sno:String,val grade:Int):Person(){
init{
println("sno is $sno")
println("grade is $grade")
}
} -
主构造函数中声明了 val/var 会自动让这参数成为该类的字段 不加就是临时的
-
继承父类为啥要带个()呢?
这就涉及了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.4.4 可见性修饰符
public:默认,都可以访问
project:当前类和子类
private:当前类中可见
internal:当前模块可见
2.4.5 Kotlin 的两个特殊类
啥是数据类?啥是单例类?
为啥Kotlin要专门搞这两个类呢? Java 的三个无意义函数
数据类 date 单例类 object
1 | fun main() { |
让我们看看如果用 Java 要怎么实现:
1 | //数据类 |
2.5 Lambda 基础
Java 的话是 Java8 出现这个 Lambda,这个 Kotlin 从第一版就支持 Lambda ,而且的话,功能的话也更加强大。可以说 Lambda 是 Kotlin 的灵魂。
这一个章节只有 Lambda 的基础,Lambda 高级技巧比如:高阶函数,DSL 后面再单独开。
2.5.1 集合
集合的函数式 API 是入门 Lambda 编程的绝佳示例。
1 | fun main() { |
2.5.2 集合的函数式 API
Lambda的定义:一小段可以作为参数传递的代码
Lambda的语法结构:{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}
这是Lambda表达式最完整的语法结构定义。首先最外层是一对大括号,如果有参数传入到 Lambda 表达式中的话,我们还需要声明参数列表,参数列表的结尾使用一个->符号,表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码(虽然不建议编写太长的代码),并且最后一行代码会自动作为Lambda表达式的返回值。
(我们有好多种简化 Lambda 的办法)
常用函数式 API 的 Lambda 变化演示
maxBy
1 | package com.swu.lmc.kotlinLearning.AndroidKotlin |
maxBy函数的工作原理
解释图:
1 | //3.简洁优化 |
map
集合中的map函数是最常用的一种函数式API,它用于将集合中的每个元素都映射成一个另外的值,映射的规则在Lambda表达式中指定,最终生成一个新的集合。比如,这里我们希望让所有的水果名都变成大写模式,就可以这样写:
1 | fun main() { |
filter
Lambda 中写过滤条件,会返回一个新的集合
1 | //4.同时使用 filter&map函数 |
2.5.3 Java 函数式 API 的使用
如果我们在Kotlin代码中调用了一个 Java 方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。
Java单抽象方法接口指的是接口中只有一个待实现方法,如果接口中有多个待实现方法,则无法使用函数式API。
1 | //Lambda 在Java 改造上的体验 |
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默认所有的参数和变量都不可为空
将空指针异常的检查提前到了编译时期,如果我们的程序存在空指针异常的风险,那么在编译的时候会直接报错,修正之后才能成功运行,这样就可以保证程序在运行时期不会出现空指针异常了。
1
2
3
4
5
6 fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
study.doHomework()
}
}
判空辅助工具
那么可为空的类型系统是什么样的呢?很简单,就是在类名的后面加上一个问号。
?.操作符。就是当对象不为空时正常调用相应的方法,当对象为空时则什么都不做。
1 | fun doStudy(study: Study?) { |
?:操作符。这个操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。
1 | val c = if (a ! = null) { |
!! 和全局变量问题
1 | //返回文本的长度 |
let函数
(Kotlin 标准函数 )既可以配合辅助判空问号,又可以解决全局变量问题
是可以处理全局变量的判空问题的,而if判断语句则无法做到这一点
1 | obj.let { obj2 -> |
直接用 != null 判空 不行,是因为全局变量的值随时都有可能被其他线程所修改,即使做了判空处理,仍然无法保证if语句中的study变量没有空指针风险。从这一点上也能体现出let函数的优势。
2.7 字符串内嵌表达式 /函数的参数默认值
字符串内嵌表达式
1 | fun main() { |
函数的参数默认值
(介绍-》用函数传参随意-》代替次构造函数)
1 | //2.函数默认参数 |
2.8 标准函数和静态方法
2.8.1 标准函数
Kotlin的标准函数指的是Standard.kt文件中定义的函数,任何Kotlin代码都可以自由地调用所
有的标准函数。
2.8.1.1 with
啥时候用:当想对同一个对象有很多操作时
它可以在连续调用同一个对象的多个方法时让代码变得更加精简
with函数接收两个参数:第一个参数可以是一个任意类型的对象,第二个参数是一个Lambda表达式。with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回。
1 | val result = with(obj) { |
1 | val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape") |
2.8.1.2 run
跟上面差不多
不会直接调用,而是要在某个对象的基础上调用;其次run函数只接收一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文。
1 | val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape") |
2.8.1.1 apply
apply函数和run函数也是极其类似的,都要在某个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。
1 | val result = obj.apply { |
2.8.2 定义静态方法
其实 Kotlin 中用单例类代替了 静态属性
不过,使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,而如
果我们只是希望让类中的某一个方法变成静态方法的调用方式该怎么办呢?这个时候就可以使
用刚刚在最佳实践环节用到的companion object了
1 | class Util { |
不过,doAction2()方法其实也并不是静态方法,companion object这个关键字实际上会
在Util类的内部创建一个伴生类,而doAction2()方法就是定义在这个伴生类里面的实例方
法。只是Kotlin会保证Util类始终只会存在一个伴生类对象,因此调用Util.doAction2()方
法实际上就是调用了Util类中伴生对象的doAction2()方法。
然而如果你确确实实需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层
- @JvmStatic注解只能加在单例类或companion object中的方法上,如果你尝试加在一个普通方法上,会直接提示语法错误。
- 顶层方法指的是那些没有定义在任何类中的方法 :直接创建一个 Kotlin 的文件,然后在这个文件中直接写的都是顶层方法。这个在Kotlin 中随便在哪,都可以直接调用顶层方法。但是在 Java 中就要
文件名.方法名
2.9 延迟初始化和密封类
2.9.1 延迟初始化
延迟初始化使用的是lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量
进行初始化,这样就不用在一开始的时候将它赋值为null了
判断是否初始化:
1 | if (!::adapter.isInitialized) { |
2.9.2 密封类
1 | package com.lmc.kotlinadvanced.demo02 |
- 这是因为当在when语句中传入一个密封类变量
作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应
的条件全部处理。- 密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。
2.10 扩展函数和运算符重载
2.10.1 扩展函数
扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数
语法结构:
1 | fun ClassName.methodName(param1: Int, param2: Int): Int { |
定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中了。
文件名虽然并没有固定的要求,但是我建议向哪个类中添加扩展函数,就定义一个同名的Kotlin文件,这样便于你以后查找。
不过通常来说,最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问
域。
2.10.2 运算符重载
Kotlin的运算符重载却允许我们让任意两个对象进行相加,或者是进行更多其他的运算操作。
运算符重载使用的是operator关键字,只要在指定函数的前面加上operator关键字,就可以实现运算符重载的功能了。
1 | class Money(val value: Int) |
Kotlin允许我们对同一个运算符进行多重重载
1 | class Money(val value: Int) { |
2.11 高阶函数
2.11.1 定义高阶函数
高阶函数的定义。如果一个函数接收另一个函数作为参数,或者返回值的类型是
另一个函数,那么该函数就称为高阶函数。函数类型。我们知道,编程语言中有整型、布尔型等字段类型,而Kotlin又增加了一个函数类型的概念。如果我们将这种函数类型添加到一个函数的参数声明或者返回值声明当中,那么这就是一个高阶函数了。
函数类型定义:(String, Int) -> Unit
因此,->左边的部分就是用来声明该函数接收什么参数的,多个参数之间使用逗号隔
开,如果不接收任何参数,写一对空括号就可以了。而->右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就使用 Unit
高阶函数声明:
1 | fun example(func: (String, Int) -> Unit) { |
高阶函数的用途:高阶函数允许让函数类型的
参数来决定函数的执行逻辑。即使是同一个高阶函数,只要传入不同的函数类型参数,那么它
的执行逻辑和最终的返回结果就可能是完全不同的。高阶函数实现的方式:
- 古板方式
- Lambda
- 匿名函数
- 成员引用
最简单的高阶函数学习示例 :
1 | fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int { |
使用高阶函数举例子:(最古板的一个)
1 | fun plus(num1: Int, num2: Int): Int { |
用 Lambda 版本
1 | fun main() { |
这才是定义高阶函数完整的语法规则,在函数类型的前面加上ClassName. 就表示这个函数类型是定义在哪个类当中的
1 | fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder { |
那么这里将函数类型定义到StringBuilder类当中有什么好处呢?==好处就是当我们调用build
函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文==,同时这也是apply函数
的实现方式。
2.11.2 内联函数的使用
高阶函数的实现原理
kotlin 下的高阶函数
1 | fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int { |
编译成 Java
1 | public static int num1AndNum2(int num1, int num2, Function operation) { |
这就是Kotlin高阶函数背后的实现原理。你会发现,原来我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式。这就表明,我们每调用一次Lambda表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。
为了解决这个问题,Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行时开销完全消除
内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字的声明即可,
2.11.3 noinline&crossinline
noinline
一个高阶函数中如果接收了两个或者更多函数类型的参数,这时我们给函数加上了inline关键字,那么Kotlin编译器会自动将所有引用的 Lambda表达式全部进行内联。
但是,如果我们只想内联其中的一个Lambda表达式该怎么办呢?
1 | inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) { |
这里使用inline关键字声明了inlineTest()函数,原本block1和block2这两个函数类型参数所引用的Lambda表达式都会被内联。但是我们在block2参数的前面又加上了一个noinline关键字,那么现在就只会对block1参数所引用的Lambda表达式进行内联了。这就是noinline关键字的作用。
为什么Kotlin还要提供一个noinline关键字来排除内联功能呢?这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。
另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。
啥叫局部返回:
1 | fun printString(str: String, block: (String) -> Unit) { |
内联函数 (非局部返回)
crossinline
如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现
中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。
1 | inline fun runRunnable(block: () -> Unit) { |
这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。
而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。
也就是说,如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。
那么是不是在这种情况下就真的无法使用内联函数了呢?也不是,比如借助crossinline关键字就可以很好地解决这个问题:
1 | inline fun runRunnable(crossinline block: () -> Unit) { |
那么这个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 | val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit() |
高阶函数升级
1 | fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) { |
其实这个就是KTX扩展库中的edit函数实现
1 | getSharedPreferences("data", Context.MODE_PRIVATE).edit { |
2.11.4.2 简化ContentValues的用法
原来:
1 | val values = ContentValues() |
Pair对象 Kotlin中一种A to B的数据结构
vararg:可变
1 | fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues { |
Kotlin中的Smart Cast功能。在When/if中判断为某一类型如果符合就会自动转换为这种类型
优化一下下
1 | fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply { |
KTX中的contentValuesOf()
1 | val values = contentValuesOf("name" to "Game of Thrones", "author" to "George Martin", |
2.12 泛型基础和委托
2.12.1 泛型基础
在一般的编程模式下,我们需要给任何一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。
定义:
泛型类:
1 | class MyClass<T> { |
泛型方法:
1 | class MyClass { |
泛型限制
不手动指定上界的时候,泛型的上界默认是Any? ,如果想不可为空那就指定为 Any
1 | class MyClass { |
小小实践:
1 | //只能作用在StringBuilder类上面 |
2.12.2 委托
委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。
2.12.2.1 类委托
将一个类的具体实现委托给另一个类去完成。
1 | //巧妙利用 委托实现自己的Set类 |
可以看到,MySet的构造函数中接收了一个HashSet参数,这就相当于一个辅助对象。然后在 Set 接口所有的方法实现中,我们都没有进行自己的实现,而是调用了辅助对象中相应的方法实现,这其实就是一种委托模式。
如果我们只是让大部分的方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,那么MySet就会成为一个全新的数据结构类,这就是委托模式的意义所在。
Kotlin中委托使用的关键字是by,我们只需要在接口声明的后面使用by关键字,再接上受委托的辅助对象,就可以免去之前所写的一大堆模板式的代码了,如下所示:
1 | class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet { |
2.12.2.2 委托属性
将一个属性(字段)的具体实现委托给另一个类去完成
语法结构:
1 | class MyClass { |
2.12.2.3 懒加载分析
本质就是一个委派
1 | private val uriMatcher by lazy { |
仿写一个自己的lazy函数
一个Later.kt文件
1 | class Later<T>(val block: () -> T) { |
2.13 infix 增强代码可读性
未被 infix 加工前
1 | if ("Hello Kotlin".startsWith("Hello")) { |
infix 开始加工
1 | infix fun String.beginsWith(prefix: String) = startsWith(prefix) |
infix 函数允许我们将函数调用时的小数点、括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编写程序,让代码看起来更加具有可读性。
infix 函数限制:
首先,infix 函数是不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中;
其次,infix 函数必须接收且只能接收一个参数
再看两个
1 | val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape") |
2.14 泛型高级特性
2.14.1 对泛型进行实化
Java中没有泛型实化这个概念,但是需要了解一个叫泛型擦除机制。
Java在JDK1.5之后才引入泛型,他的实现就是通过泛型擦除机制。泛型对于类
型的约束只在编译时期存在 ,JVM是识别不出来我们在代码中指定的泛型类型。所以用不了 T::class.java 这种语法,因为 T的实际类型在运行的时候是已经抹去的。 不能拿到泛型的实际类型(泛型无法实化)
Kotlin 的解决方案:inline(内联函数代码替换)+ reified
1 | inline fun <reified T> getGenericType() = T::class.java |
泛型实化的应用:优化 Intent 的使用
1 | //之前 |
Kotlin将能够识别出指定泛型的实际类型,并启动相应的Activity。
2.14.2 泛型的协变
在开始学习协变和逆变之前,我们还得先了解一个约定。一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为in位置,而它的返回值是输出数据的地方,因此可以称它为out位置
1 | open class Person(val name: String, val age: Int) |
Student 是 Persion 的子类,但是 List
安全隐患举例:
1 | class SimpleData<T> { |
即使Student是Person的子类,SimpleData
问题所在:在handleSimpleData()方法中向SimpleData
泛型协变定义:假如定义了一个MyClass
的泛型类,其中 A 是 B 的子类型,同时MyClass又是MyClass的子类型,那么我们就可以称MyClass 在 T 这个泛型上是协变的。
1 |
|
让 SimpleData 在泛型T上是协变的。
谈一下 List 简化源码:
1 | public interface List<out E> : Collection<E> { |
2.14.3 泛型的逆变
逆变与协变却完全相反。那么这里先引出定义吧,假如定义了一个
MyClass的泛型类,其中A是B的子类型,同时 MyClass 又是MyClass 的子类型,那么我们就可以称 MyClass 在T这个泛型上是逆变的。
1 | interface Transformer<T> { |
Transformer
1 | interface Transformer<in T> { |
为什么逆变的 T 不能出现在 out 位置上?
1 | interface Transformer<in T> { |
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 | dependencies { |
新建一个文件
开启协程
1 | fun main() { |
Global.launch函数每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟着一起结束。
改进:
1 | //1 能打印出来那句话了 |
加餐小知识:
delay()是非阻塞的挂起函数,它只会挂起当前协程,不会影响其他协程的运行;(这个函数只能在协程的作用域和其他挂起函数中调用)
但是 Thread.sleep 会阻塞当前的线程,这时候当前线程下的所有协程都会被阻塞。
让应用程序在协程中所有代码都运行完了之后才结束
1 | fun main() { |
runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题 。
创建多个协程
1 | fun main() { |
注意这里的launch函数和我们刚才所使用的GlobalScope.launch函数不同。首先它必须在
协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。相比而言,
GlobalScope.launch函数创建的永远是顶层协程,这一点和线程比较像,因为线程也没有层级这一说,永远都是顶层的。
一小段测试代码:
1 | fun main() { |
suspend关键字,使用它可以将任意函数声明成挂起函数(但无法给它提供协程作用域的),而挂起函数之间都是可以互相调用的
1 | suspend fun printDot() { |
怎末给任意挂起函数提供协程作用域 ?
1 | suspend fun printDot() = coroutineScope { |
coroutineScope函数和runBlocking函数还有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起。
1 | //验证上面案例 |
虽然看上去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 | val job = GlobalScope.launch { |
实际项目更常见使用方法
1 | val job = Job() |
所有调用CoroutineScope的launch函数所创建的协程,都会被关联在Job对象的作用域下面。这样只需要调用一次cancel()方法,就可以将同一作用域内的所有协程全部取消
小总结:
CoroutineScope()函数 更适合实际项目/在main函数中的一些学习测试 还是用runBlocking函数最方便
launch函数只能用于执行一段逻辑,却不能获取执行的结果(因为返回值永远是一个Job对象) ,那我想要==创建一个协程并获取它的执行结果==怎末做?
async函数必须在协程作用域当中才能调用,它会创建一个新的子协程并返回一个Deferred对象,如果我们想要获取async函数代码块的执行结果,只需要调用Deferred对象的await()方法即可
1 | fun main() { |
事实上,在调用了async函数之后,代码块中的代码就会立刻开始执行。当调用await()方法时,如果代码块中的代码还没执行完,那么await()方法会将当前协程阻塞住,直到可以获得async函数的执行结果。
小小验证:
1 | //两个协程在串行 |
withContext()挂起函数
1 | fun main() { |
协程他妈这么牛是不是就可以不用线程了?
Android中要求网络请求必须在子线程中进行,即使你开启了
协程去执行网络请求,假如它是主线程当中的协程,那么程序仍然会出错。这个时候我们就应
该通过线程参数给协程指定一个具体的运行线程。
线程参数
-
Dispatchers.Default表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。
-
Dispatchers.IO表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用 Dispatchers.IO。
-
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 | suspend fun request(address: String): String { |
可以看到,request()函数是一个挂起函数,并且接收一个address参数。在request()函
数的内部,我们调用了刚刚介绍的suspendCoroutine函数,这样当前协程就会被立刻挂起,而Lambda表达式中的代码则会在普通线程中执行。接着我们在Lambda表达式中调用
HttpUtil.sendHttpRequest()方法发起网络请求,并通过传统回调的方式监听请求结果。
如果请求成功就调用Continuation的resume()方法恢复被挂起的协程,并传入服务器响应
的数据,该值会成为suspendCoroutine函数的返回值。如果请求失败,就调用
Continuation 的 resumeWithException() 恢复被挂起的协程,并传入具体的异常原因。
用一下
1 | //访问百度首页 |
由于 getBaiduResponse()是一个挂起函数,因此当它调用了request()函数时,当前的协程就会被立刻挂起,然后一直等待网络请求成功或失败后,当前协程才能恢复运行。这样即使不使用回调的写法,我们也能够获得异步网络请求的响应数据,而如果请求失败,则会直接进入catch语句当中。
事实上,suspendCoroutine函数几乎可以用于简化任何回调的写法
比如之前的 Retrofit 发起网络请求
1 | //之前 |
每次发起网络请求都要进行一次try catch处理也比较麻烦 ?
选择不处理。在不处理
如果发生了异常就会一层层向上抛出,一直到被某一层的函数处理了为止。因此,
我们也可以在某个统一的入口函数中只进行一次try catch,从而让代码变得更加精简。
2.16 编写好看的工具方法
2.16.1 求 N 个数的最大最小值
利用泛型
1 | fun <T : Comparable<T>> max(vararg nums: T): T { |
2.16.2 简化 Toast
1 | fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) { |
2.16.3 简化 Snackbar
1 | fun View.showSnackbar(text: String, actionText: String? = null, |
2.17 DSL 构建专有的语法结构
DSL的全称是领域特定语言(Domain Specific Language)
实现办法:
- infix也算
- 更多的是高级语言
2.17.1 模仿gradle
新建一个DSL.kt文件
1 | class Dependency { |
2.17.2 动态生成HTML表格代码
DSL.kt文件中:
1 | class Td { |
用Kotlin的DSL来动态生成一个表格所对应的HTML代码:
1 | fun main() { |
1 | fun main() { |
2.18 Java 和 Kotlin 代码之间的转换
就是有这么一个问题:现在当然绝大多数App都是在Kotlin开发,但是其实有些老的App他是用Java开发的,所以说我们遇到了这种老的App怎么让它里面的Java转换成Kotlin语言。
其实AS为我们想到了这一点。
-
一段Java代码转Kotlin:
直接把Java代码粘贴到一个kt文件,他会提示转化成Kotlin,你点下好的就行了。只不过这种是比较僵硬的,不会自动应用Kotlin各种高级特性。
-
一个Java文件转Kotlin:
第一种方法:
第二种方法:
-
Kotlin文件看Java形式源码:
三,Activity
3.1 Activity 是什么
Activity是最容易吸引用户的地方,它是一种可以包含用户界面的组件,主要用于和用户进行交互。
3.2 怎么使用/创建 Activity
3.2.1 手动创建 Activity
3.2.2 布局创建和加载
Android程序的设计讲究逻辑和视图分离,最好每一个Activity都能对应一个布
局。布局是用来显示界面内容的
如果你需要在XML中引用一个id,就使用@id/id_name这种语法,而如果你需要在XML中定义一个id,则要使用@+id/id_name这种语法
这里使用 match_parent 表示让当前元素和父元素一样宽。android:layout_height 指定了当前元素的高度,这里使用 wrap_content 表示当前元素的高度只要能刚好包含里面的内容就行
layout 中用 xml 开始写
3.2.3 在AndroidManifest文件中注册和配置主启动器
1 |
|
另外需要注意,如果你的应用程序中没有声明任何一个Activity作为主 Activity,这个程序仍然是可以正常安装的,只是你无法在启动器中看到或者打开这个程序。这种程序一般是作为第三方服务供其他应用在内部进行调用的。
上面就是手动创建了一个 Acticity
3.2.4 Activity中使用Toast
Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间
带 show() 啊!!!
1 | class FirstActivity : AppCompatActivity() { |
注意:
- 以前有在后面 as 的,有在前面显式注明变量类型的,现在是直接泛型指定
- 那个插件是 ‘kotlin-android-extensions’ 也被弃用了,第一行代码这个地方有所出入。(AS4.1之后不默认引用,而且弃用了现在)
- 关于获取xml中控件的手段有好几种,这里贴上一篇博客:https://lmc.pub/2022/07/30/2022-07-30-《吐血推荐!全网最硬核Android视图绑定》/
点下按钮,效果图:
备注:findViewById() 少用,用这个插件生成的对应控件 id
1 | firstButton.setOnClickListener{ |
3.2.5 Activity使用Menu
main.xml中是下面这个:
1 |
|
重写onCreateOptionsMenu()方法
Ctrl+O找到这个或者直接打名字会有提示
下图眼花选错了,是onCreateOptionsMenu
1 | //设计菜单 |
搞出来什么样子:
3.2.6 销毁 Activity
代码形式就是一个 finish() 函数没了
3.3 Intent 让 Activity 穿梭
Intent一般可用于启动Activity、启动Service以及发送广播等场景
3.3.1 显式 Intent
新建 Activity
-
建立Intent
-
启动Activity
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 | val intent = Intent("com.lmc.myfirstemptyactivitytest.ACTION_START") |
1 | <activity |
每个Intent中只能指定一个action,但能指定多个category
上面是在自己 app 中 跳跃,其实也可以在不同的 app 中 Intent
在第二个页面中写
1 | //点这个按钮就会打开浏览器然后进入那个网址 |
与此对应,我们还可以在
标签中再配置一个标签,用于更精确地指
定当前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 | <activity |
https 协议响应不了,因为一般系统会绑定打开 https 协议的浏览器
除了https协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。
1 | class ThirdActivity : AppCompatActivity() { |
3.3.3 传递数据
3.3.3.1 向下传递数据
数据发送 Activity:
1 | val data = "Hello SecondActivity" |
接受方:
1 | //接受 Activity1 发我的信息并显示 |
3.3.3.2 向上传递数据
其实Activity类中还有一个用于启动Activity的startActivityForResult()方法,但它期望在Activity销毁的时候能够返回一个结果给上
一个Activity。
注意:!!!《第一行代码》的startActivityForResult
已经被废弃,现在是用下面演示的 ActivityResult API了
-
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
33class 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)
}
}
} -
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给用户。
3.4.2 Activity状态
- 运行状态:在栈顶,全部可见,最不会回收
- 暂停状态:不在栈顶,但是看得见,内存极低才会回收
- 停止状态:不在栈顶,页面已经看不见,内存不够就会回收
- 销毁状态:已经出栈了,最爱回收
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
3.4.4 体验Activity的生命周期
怎么设置一个 Activity 为 Dialog 类型的?
1
2
3 <activity android:name=".DialogActivity"
android:theme="@style/Theme.AppCompat.Dialog">
</activity>
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 | override fun onSaveInstanceState(outState: Bundle) { |
另外,当手机的屏幕发生旋转的时候,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的新实例
3.5.2 singleTop
适用最多的
singleTop模式。当Activity的启动模式指定为singleTop,在启动Activity时如果发现返回栈的栈顶已经是该Activity,则认为可以直接使用它,不会再创建新的Activity实例
android:launchMode=“singleTop”
1 | <activity |
FirstActivity中onCreate()方法的代码 :
1 | override fun onCreate(savedInstanceState: Bundle?) { |
3.5.3 singleTask
使用singleTop模式可以很好地解决重复创建栈顶Activity的问题,但是正如你在上一节所看到的,如果该Activity并没有处于栈顶的位置,还是可能会创建多个Activity实例的
**让某个Activity在整个应用程序的上下文中只存在一个实例 **
当Activity的启动模式指定为singleTask,每次启动该Activity时,系统首先会在返回栈中检查是否存在该Activity的实例,如果发现已经存在则直接使用该实例,并把在这个Activity之上的所有其他Activity统统出栈,如果没有发现就会创建一个新的Activity实例。
3.5.4 singleInstance
指定为singleInstance模式的Activity会启用一个新的返回栈来管理这个Activity(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈)
该 activity 始终是其任务中的唯一 activity。
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中多个实例中启动。
-
以MainActivity为起点,启动SingleInstancePerTaskActivity:
-
当SingleInstancePerTaskActivity位于当前Task top时启动SingleInstancePerTaskActivity:
发现SingleInstancePerTaskActivity不会重复启动,而是现有的实例收到Activity.onNewIntent回调:
-
当SingleInstancePerTaskActivity没有位于当前Task top时启动SingleInstancePerTaskActivity:
-
当SingleInstancePerTaskActivity存在的Task没有处于前台时去启动SingleInstancePerTaskActivity:
-
结合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中多次实例化
这篇文章讲的非常到‘胃’: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
-
建个 baseActivity
1
2
3
4
5
6open class BaseActivity :AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
Log.d("BaseActivity",javaClass.simpleName)
}
}Bug记录:
按上面这么搞那个《最佳实践》的一二两个都一直不行,找了好久找出来了。是这里那个 onCreate 参数用错了,改成用一个参数的就好了
-
然后把所有的 Activity 继承这个 BaseActivity
3.6.2 随时随地退出程序
1 | object ActivityCollector { |
然后在之前那个 BaseActivity 的拆功
3.6.3 启动Activity的最佳写法
养成习惯:写 Activity 的时候,每个前面都加一个
1 | class SecondActivity : BaseActivity() { |
然后大家就用这个来启动想要启动的 activity
四,UI
4.1 常用控件
4.1.1 TextView
1 |
|
4.1.2 Button
1 | <Button |
接口实现监听器:
1 | class MainActivity : AppCompatActivity(),View.OnClickListener { |
函数式API实现监听器:
4.1.3 EditText
1 | <EditText |
可以配合Button玩
4.1.4 ImageView
在res目录下再新建一个drawable-xxhdpi目录
1 | <ImageView |
4.1.5 ProgressBar
1 | <ProgressBar |
控件的可见性,使用的是setVisibility()方法,允许传入View.VISIBLE、View.INVISIBLE和View.GONE这3种值。
1 | R.id.button03 -> { |
4.1.6 AlertDialog
1 | R.id.button05 -> { |
4.2 布局
一个丰富的界面是由很多个控件组成的,布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。当然,布局的内部除了放置控件外,也可以放置布局,通过多层布局的嵌套,我们就能够完成一些比较复杂的界面实现。
4.2.1 LinearLayout
这个布局会将它所包含的控件在线性方向上依次排列
默认是横着的
布局和控件的关系 :
android:gravity 用于指定文字在控件中的对齐方式,而 android:layout_gravity 用于指定控件在布局中的对齐方式
当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。因为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。
android:layout_weight。这个属性允许我们使用比例的方式来指定控件的大小 就是权重
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
4.2.2 RelativeLayout
1 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
注意,当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面,不然会出现找不到id的情况。
RelativeLayout 中还有另外一组相对于控件进行定位的属性,android:layout_alignLeft 表示让一个控件的左边缘和另一个控件的左边缘对齐,android:layout_alignRight表示让一个控件的右边缘和另一个控件的右边缘对齐。此外,还有 android:layout_alignTop 和 android:layout_alignBottom,道理都是一样的,
4.3.3 FrameLayout
FrameLayout又称作帧布局,这种布局没有丰富的定位方式,所有的控件都会默认摆放在布局的左上角。 想要不堆在左上角就用 gravity中 left rigth 来指定
1 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
4.3 自定义控件
我们所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间
接继承自ViewGroup的。View是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩
形区域,并能响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View的基础上
又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多子View和子
ViewGroup,是一个用于放置控件和布局的容器。
常用控件和布局的继承结构 :
4.3.1 引入布局
一般我们的程序中可能有很多个Activity需要这样的标题栏,如果在每个Activity的布局中都编写一遍同样的标题栏代码,明显就会导致代码的大量重复。这时我们就可以使用引入布局的方式来解决这个问题
-
在 layout 中定义一个 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
<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> -
注意:这里 的 include 和下面 自定义控件那个,只能有一个。不然会显示不出来。因为是继承关系撒
-
4.3.2 创建自定义控件
比如标题栏中的返回按钮,其实不管是在哪一个Activity中,这个按钮的功能都是相同的,即销毁当前 Activity。而如果在每一个Activity中都需要重新注册一遍返回按钮的点击事件,无疑会增加很多重复代码,这种情况最好是使用自定义控件的方式来解决
-
首先 新建一个类 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
38package 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
*/
} -
在页面的布局文件中添加这个 自定义控件
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"
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 简单用法
-
在 layout 界面写一个 ListView
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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> -
在 MainActivity 中写逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package 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
-
准备图片资源
-
写 ListView 适配器的适配类型
-
写 ListView 的子项是是什么布局
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="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都在垂直方向上居中显示
-
写自定义适配器:!!!(难点)
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
28package 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()方法设置显示的图片和文字,最后将布局返回,这样我们自定义的适配器就完成了。
-
修改 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
40package 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 | package com.lmc.listviewtest |
4.4.4 ListView 点击事件
1 | //下面是加上点击效果 |
4.5 RecyclerView
4.5.1 基本用法
-
app:build.gradle 中加一行依赖关系
1
implementation 'androidx.recyclerview:recyclerview:1.2.1'//RecycleView 控件
-
修改 activity_main.xml 中的代码
ViewHolder的主构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外层布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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> -
准备适配器
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
28package 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
} -
改 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
34package 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 可以用于实现瀑布流布局。
横向滚动
-
修改 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> -
改 MainActivity
1
layoutManager.orientation = LinearLayoutManager.HORIZONTAL//加上这个横着显示 加在 onCreate 里面,一行代码就行
瀑布式布局
-
修改 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> -
修改 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
54package 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 点击事件
-
修改 FruitAdapter
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
51package 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图片,能够指定哪些区域可以被拉伸、哪些区域不可以。
我们可以在图片的4个边框绘制一个个的小黑点,在上边框和左边框绘制的部分表示当图片需要拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分表示内容允许被放置的区域。使用鼠标在图片的边缘拖动就可以进行绘制了,按住Shift键拖动可以进行擦除。
4.6.2 写聊天界面
-
加依赖:
1
implementation 'androidx.recyclerview:recyclerview:1.2.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
<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> -
定义消息实体类
1
2
3
4
5
6
7
8package 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关键字
-
写 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
<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
<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 | package com.lmc.android21 |
-
写 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
35package 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
} -
修改 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
50package 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("")//清空输入框中的内容
}
}
}
}
}
五,Fragment
Fragment是一种可以嵌入在Activity当中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间,因而在平板上应用得非常广泛。
平板双页设计:
5.1 使用方法
用这个代替《第一行代码》第三版中老师说的那个AS自带虚拟机
5.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
<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>
<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> -
新建左右两个对应的类
注意每个类继承的都是
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
30package 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)
}
} -
修改 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
<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
-
新建 another_right_fragment.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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> -
新建 AnotherRightFragment
1
2
3
4
5
6
7
8
9
10
11
12
13
14package 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)
}
} -
改动 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
<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> -
下面我们将在代码中向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
24package 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 中实现返回栈
1 | transaction.addToBackStack(null)//加上这个让出现了动态画面然后返回是回到点击之前,而不是直接退出 |
5.1.4 Fragment和Activity之间的交互
5.1.4.1 如何在Activity中调用Fragment里的方法
-
像这样获得 Fragment 的实例
val fragment = leftFrag as LeftFragment
-
然后直接用 这个变量的方法就可以的
5.1.4.2 Fragment中又该怎样调用Activity
1 | if (activity != null) { |
既然Fragment和Activity之间的通信问题已经解决了,那么不同的Fragment之间可不可以进行通信呢?说实在的,这个问题并没有看上去那么复杂,它的基本思路非常简单:首先在一个Fragment中可以得到与它相关联的Activity,然后再通过这个Activity去获取另外一个Fragment的实例,这样就实现了不同Fragment之间的通信功能。因此,这里我们的回答是肯定的。
5.2 Fragment的生命周期
5.2.1 Fragment的状态和回调
状态
- 运行状态
当一个Fragment所关联的Activity正处于运行状态时,该Fragment也处于运行状态。 - 暂停状态
当一个Activity进入暂停状态时(由于另一个未占满屏幕的Activity被添加到了栈顶),与它相关联的Fragment就会进入暂停状态。 - 停止状态
当一个Activity进入停止状态时,与它相关联的Fragment就会进入停止状态,或者通过调用FragmentTransaction的remove()、replace()方法将Fragment从Activity中移除,但在事务提交之前调用了addToBackStack()方法,这时的Fragment也会进入停止
状态。总的来说,进入停止状态的Fragment对用户来说是完全不可见的,有可能会被系统回收。 - 销毁状态
Fragment总是依附于Activity而存在,因此当Activity被销毁时,与它相关联的 Fragment就会进入销毁状态。或者通过调用FragmentTransaction的remove()、replace()方法将Fragment从Activity中移除,但在事务提交之前并没有调用addToBackStack()方法,这时的Fragment也会进入销毁状态。
回调
- onAttach():当Fragment和Activity建立关联时调用。
- onCreateView():为Fragment创建视图(加载布局)时调用。
- onActivityCreated():确保与Fragment相关联的Activity已经创建完毕时调用。
- onDestroyView():当与Fragment关联的视图被移除时调用。
- onDetach():当Fragment和Activity解除关联时调用。
1 | package com.lmc.a23 |
由于AnotherRightFragment替换了RightFragment,此时的RightFragment进入了停止状态,因此onPause()、onStop()和onDestroyView()方法会得到执行。当然,如果在替换的时候没有调用addToBackStack()方法,此时的RightFragment就会进入销毁状态,onDestroy()和onDetach()方法就会得到执行
另外值得一提的是,在Fragment中你也可以通过onSaveInstanceState()方法来保存数据,因为进入停止状态的Fragment有可能在系统内存不足的时候被回收。保存下来的数据在 onCreate()、onCreateView()和onActivityCreated()这3个方法中你都可以重新得到,它们都含有一个Bundle类型的savedInstanceState参数。具体的代码我就不在这里展示了,如果你忘记了该如何编写,可以参考3.4.5小节
5.3 动态加载布局的技巧
程序能够根据设备的分辨率或屏幕大小,在运行时决定加载哪个布局
注意:下面内容受手机平板虚拟机的尺寸差异过大很可能你的代码是正确的但是运行起来得不到预期效果,其实是设备的尺寸问题
5.3.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> -
在 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> -
可以看到,layout/activity_main布局只包含了一个Fragment,即单页模式,而layoutlarge/ activity_main布局包含了两个Fragment,即双页模式。其中,large就是一个限定符,那些屏幕被认为是large的设备就会自动加载layout-large文件夹下的布局,小屏幕的设备则还是会加载layout文件夹下的布局
-
5.4.2 使用最小宽度限定符
最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。
5.4 Fragment 实战 新闻页面
-
加上那个
kotlin-android-extensions
插件和 RecycleView 的插件 -
写 新闻 实体类
1
2
3package com.lmc.android26
class News(val title:String,val content:String) -
写 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
<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> -
写 NewsContentFragment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package 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 // 刷新新闻的内容
}
} -
为了单页面中也能用,加一个 NewsContentActivity 。然后修改 activity_news_content.xml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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布局的内容自动加了进来。
-
然后再写 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
42package 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界面
}
}
} -
接下来还需要再创建一个用于显示新闻列表的布局,新建news_title_frag.xml
1
2
3
4
5
6
7
8
9
10
<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> -
新闻列表子项布局
android:padding表示给控件的周围加上补白,这样不至于让文本内容紧靠在边缘上;android:maxLines设置为1表示让这个TextView只能单行显示;android:ellipsize用于设定当文本内容超出控件宽度时文本的缩略方式,这里指定成end表示在尾部进行缩略
1
2
3
4
5
6
7
8
9
10
11
12
13
<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"/> -
一个用于展示新闻列表的地方。这里新建NewsTitleFragment作为展示新闻列表的Fragment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package 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
}
} -
修改 activity_main.xml
1 | <?xml version="1.0" encoding="utf-8"?> |
-
新建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
<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> -
在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
51package 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
}
} -
向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
77package 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()
}
}
上图分享ppt/PDF地址:
https://wwc.lanzoum.com/b03dcu3ob
密码:f5ht
六,广播机制
6.1 广播机制简介
BroadcastReceiver
广播类型
-
标准广播(normal broadcasts)
异步执行
同一时间全给出去了,拦截不了,但效率高.
-
有序广播(ordered broadcasts)
同步执行
6.2 接受系统广播
啥叫 系统广播?
Android内置了很多系统级别的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。比如手机开机完成后会发出一条广播,电池的电量发生变化会发出一条广播,系统时间发生改变也会发出一条广播,等等。
6.2.1 动态注册监听时间变化
注册 BroadcastReceiver的方式一般有两种:在代码中注册和在AndroidManifest.xml中注册。其中前者也被称为动态注册,后者也被称为静态注册。
如何创建一个BroadcastReceiver呢?
其实只需新建一个类,让它继承自 BroadcastReceiver,并重写父类的 onReceive() 方法就行了。这样当有广播到来时,onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。
动态注册的BroadcastReceiver一定要取消注册才行,这里我们是在 onDestroy()方法中通过调用unregisterReceiver()方法来实现的。
1 | package com.lmc.android28 |
完整广播列表地址:
6.2.2 静态注册实现开机启动
动态注册的BroadcastReceiver可以自由地控制注册与注销,在灵活性方面有很大的优势。但是它存在着一个缺点,即必须在程序启动之后才能接收广播,因为注册的逻辑是写在 onCreate()方法中的。
静态注册:可以让程序在未启动的情况下也能接收广播
在Android 8.0系统之后,所有隐式广播都不允许使用静态注册的方式来接收了。隐式广播指的是那些没有具体指定发送给哪个应用程序的广播,大多数系统广播属于隐式广播,但是少数特殊的系统广播目前仍然允许使用静态注册的方式来接收。这些特殊的系统广播列表详见
https://developer.android.google.cn/guide/components/broadcastexceptions.html。
Exported 属性表示是否允许这个 BroadcastReceiver 接收本程序以外的广播,Enabled 属性表示是否启用这个 BroadcastReceiver。
静态的BroadcastReceiver一定要在AndroidManifest.xml文件中注册才可以使用。如果是使用Android Studio的快捷方式创建的BroadcastReceiver,因此注册这一步已经自动完成了。
1 |
|
由于Android系统启动完成后会发出一条值为android.intent.action.BOOT_COMPLETED 的广播,因此我们在
标签中又添加了一个 标签,并在里面声明了相应的action。 另外,这里有非常重要的一点需要说明。Android 系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,必须在 AndroidManifest.xml 文件中进行权限声明,否则程序将会直接崩溃。比如这里接收系统的开机广播就是需要进行权限声明的,所以我们在上述代码中使用
标签声明android.permission.RECEIVE_BOOT_COMPLETED 权限。 到目前为止,我们在BroadcastReceiver的onReceive()方法中只是简单地使用Toast提示了一段文本信息,当你真正在项目中使用它的时候,可以在里面编写自己的逻辑。需要注意的是,不要在onReceive()方法中添加过多的逻辑或者进行任何的耗时操作,因为 BroadcastReceiver 中是不允许开启线程的,当 onReceive()方法运行了较长时间而没有结束时,程序就会出现错误。
6.3 发送自定义广播
6.3.1 发送标准广播
1 | <receiver |
1 | button.setOnClickListener { |
首先构建了一个 Intent 对象,并把要发送的广播的值传入。然后调用 Intent 的setPackage() 方法,并传入当前应用程序的包名。packageName 是 getPackageName() 的语法糖写法,用于获取当前应用程序的包名。最后调用 sendBroadcast() 方法将广播发送出去,这样所有监听 com.example.broadcasttest.MY_BROADCAST 这条广播的 BroadcastReceiver 就会收到消息了。此时发出去的广播就是一条标准广播。
在Android8.0系统之后,静态注册的BroadcastReceiver是无法接收隐式广播的,而默认情况下我们发出的自定义广播恰恰都是隐式广播。因此这里一定要调用setPackage()方法,指定这条广播是发送给哪个应用程序的,从而让它变成一条显式广播,否则静态注册的BroadcastReceiver将无法接收到这条广播。
可以在Intent中携带一些数据传递给相应的
BroadcastReceiver,这一点和Activity的用法是比较相似
6.3.2 发送有序广播
1 | button.setOnClickListener { |
可以看到,发送有序广播只需要改动一行代码,即将 sendBroadcast() 方法改成
sendOrderedBroadcast() 方法。sendOrderedBroadcast() 方法接收两个参数:第一个参数仍然是 Intent;第二个参数是一个与权限相关的字符串,这里传入 null 就行了。
如何设定BroadcastReceiver的先后顺序呢?
1 | <intent-filter android:priority="100"> |
我们通过android:priority属性给BroadcastReceiver设置了优先级,优先级比较高的BroadcastReceiver就可以先收到广播。 如果在onReceive()方法中调用 abortBroadcast()方法,就表示将这条广播截断,后面的 BroadcastReceiver 将无法再接收到这条广播。
在MyBroadcastReceiver.kt
1 | package com.lmc.android28 |
6.4 广播实践:强制下线
项目描述:
无论在哪一个界面,界面上弹出来一个对话框,中间用户无法操作,必须点击确认然后去重新登录
6.4.1 准备
- ActivityCollector和BaseActivity
6.4.2 登陆界面处理
-
一个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
<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
27package 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 |
|
kt:
1 | package com.lmc.broastcastbestpractice |
强制用户下线的逻辑并不是写在MainActivity里的,而是应该写
在接收这条广播的BroadcastReceiver里。这样强制下线的功能就不会依附于任何界面了,不
管是在程序的任何地方,只要发出这样一条广播,就可以完成强制下线的操作了。
6.4.4 接受广播
怎末解决接受广播的,在任何一个地方都可以弹出来一个控件堵塞用户操作?
接下来我们就需要创建一个BroadcastReceiver来接收这条强制下线广播。唯
一的问题就是,应该在哪里创建呢?由于BroadcastReceiver中需要弹出一个对话框来阻塞用
户的正常操作,但如果创建的是一个静态注册的BroadcastReceiver,是没有办法在
onReceive()方法里弹出对话框这样的UI控件的,而我们显然也不可能在每个Activity中都注
册一个动态的BroadcastReceiver。
那么到底应该怎么办呢?答案其实很明显,只需要在BaseActivity中动态注册一个
BroadcastReceiver就可以了,因为所有的Activity都继承自BaseActivity。
1 | package com.lmc.broastcastbestpractice |
为什么要这样写呢?之前不都是在onCreate()和onDestroy()方法里注册和取消注册
BroadcastReceiver的吗?这是因为我们始终需要保证只有处于栈顶的Activity才能接收到这
条强制下线广播,非栈顶的Activity不应该也没必要接收这条广播,所以写在onResume()和
onPause()方法里就可以很好地解决这个问题,当一个Activity失去栈顶位置时就会自动取消
BroadcastReceiver的注册。
6.4.5 AndroidManifest
主Activity改为LoginActivity
七,数据存储方案/持久化技术
7.1 持久化技术简介
数据持久化:将那些内存中的瞬时数据保存到存储设备中
Android系统中主要提供了3种方式用于简单地实现数据持久化功能:
文件存储
SharedPreferences存储
数据库存储。
7.2 文件存储
不对存储的内容进行任何格式化处理,数据原封不动地保存到文件中/
适合:存储一些简单的文本数据或二进制数据。
不适合:较为复杂的结构化数据
7.2.1 将数据存储到文件中
Context类中的openFileOutput()方法
【接收参数】:
文件名(所有的文件都默认存储到/data/data/
/files/目录下) 文件的操作模式
MODE_PRIVATE(默认)同名覆盖
MODE_APPEND 同名追加
openFileOutput()方法返回的是一个 FileOutputStream 对象,之后用 Java 流的方式将数据写入文件。
1 | fun save(inputText: String) { |
use函数:Kotlin提供的一个内置扩展函数。保证在 Lambda 表达式中的代码全部执行完之后自动将外层的流关闭
Kotlin是没有异常检查机制(checked exception)的。这意味着使用Kotlin编写的所有代码都不会强制要求你进行异常捕获或异常抛出。上述代码中的try catch代码块是参照 Java 的编程规范添加的,即使你不写try catch代码块,在Kotlin中依然可以编译通过。
activity_main.xml
1 | xml<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
在 MainActivity 中
1 | class MainActivity : AppCompatActivity() { |
查看:
Device File Explorer
Ctrl + Shift + A
地址:/data/data/com.example.filepersistencetest/files/
7.2.2 从文件中读取数据
Context类中还提供了一个openFileInput()方法
接收一个参数:文件名
系统会自动到/data/data/
/files/目录下加载这个文件,并返回一个FileInputStream对象,得到这个对象之后,再通过流的方式就可以将数据读取出
1 | fun load(): String { |
forEachLine函数
将读到的每行内容都回调到Lambda表达式中,我们在 Lambda 表达式中完成拼接逻辑即可
改一下 MainActivity 中的代码:
1 | class MainActivity : AppCompatActivity() { |
setSelection()方法将输入光标移动到文本的末尾位置以便继续输入
7.3 SharedPreferences存储
键值对的方式来存储数据
支持数据类型存储
7.3.1 将数据存储到SharedPreferences中
要想使用SharedPreferences存储数据,首先需要获取SharedPreferences对象。
Android 中主要提供了以下两种方法用于得到 SharedPreferences 对象。
获取 SharedPreferences
-
Context类中的getSharedPreferences()方法
接收参数:
文件的名称
(SharedPreferences文件都是存放在/data/data/
/shared_prefs/目录下的) 操作模式 默认的MODE_PRIVATE
它和直接传入0的效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。
-
Activity类中的getPreferences()方法
操作模式参数
因为使用这个方法时会自动将当前Activity的类名作为 SharedPreferences的文件名。
向SharedPreferences文件中存储数据步骤:
(1) 调用SharedPreferences对象的edit()方法获取SharedPreferences.Editor对象。
(2) 向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法,添加一个字符串则使用putString()方法,以此类推。
(3) 调用apply()方法将添加的数据提交,从而完成数据存储操作。
xml:
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | class MainActivity : AppCompatActivity() { |
SharedPreferences文件是使用XML格式来对数据进行管理的。
7.3.2 从SharedPreferences中读取数据
这些get方法都接收两个参数:
第一个参数是键,传入存储数据时使用的键就可以得到相应的值了;
第二个参数是默认值,即表示当传入的键找不到对应的值时会以什么样的默认值进行返回。
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | class MainActivity : AppCompatActivity() { |
首先通过getSharedPreferences()方法得到了SharedPreferences对象,然后分别调用它的getString()、getInt()和getBoolean()方法,去获取前面所存储的姓名、年龄和是否已婚,如果没有找到相应的值,就会使用方法中传入的默认值来代替,最后通过Log将这些值打印出来。
7.3.3 实现记住密码功能
在上一个章节的实践中有一个登陆界面
activity_login.xml
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
LoginActivity中的代码:
1 | class LoginActivity : BaseActivity() { |
CheckBox。这是一个复选框控件,用户可以通过点击的方式进行选中和取消,我们就使用这个控件来表示用户是否需要记住密码。
7.4 SQLite数据库
文件存储和SharedPreferences存储:保存一些简单的数据和键值对,
SQLite:大量复杂的关系型数据
SQLiteOpenHelper帮助类(抽象类) 适用于创建和升级数据库
7.4.1 创建数据库
SQLiteOpenHelper是 抽象类 -> 继承
必须实现的两抽象方法:onCreate()和 onUpgrade() 创建和升级数据库的逻辑。
构造方法,函数较少的构造方法中接收4个参数:
第一个参数:Context:上下文
第二个参数:数据库名
第三个参数:在查询数据的时候返回一个自定义的Cursor,一般写null
第四个参数:当前数据库版本号,用于数据库升级
两实例方法:getReadableDatabase()和 getWritableDatabase()。创建/打开一个现有的数据库(数据库存在->打开,否则->创建),返回一个对数据库进行读写操作的对象。
区别:当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式打开数据库,而 getWritableDatabase() 方法会出现异常。
创建数据库步骤:
构建SQLiteOpenHelper的实例
调用它的getReadableDatabase()或getWritableDatabase()方法了
当数据库创建的时候,重写的onCreate()方法也会得到执行,所以通常会在这里处理一些创建表的逻辑。
(数据库文件会存放在/data/data/
/databases/目录下)
建立一个 BookStore.db 然后一张 Book 表
1 | create table Book ( |
integer表示整型,real表示
浮点型,text表示文本类型,blob表示二进制类型
MyDatabaseHelper:
1 | package com.lmc.android34 |
MainActivity:
1 | package com.lmc.android34 |
验证:
Device File Explorer (Save As) + Database Navigator 插件 【细节注意,玄学Bug】
7.4.2 升级数据库
一般都是再加一张表
在 MyDatabaseHelper
1 | package com.lmc.android34 |
记得在 MainActivity 中那个版本号要改的比之前那个要大
7.4.3 添加数据
SQLiteOpenHelper的getReadableDatabase() 或 getWritableDatabase() 方法会返回一个 SQLiteDatabase 对象,借助这个对象就可以对数据进行 CRUD 操作了。
SQLiteDatabase 提供了一个 insert() 方法,接收 3 个参数:
第一个参数:表名;
第二个参数:用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般用不到这直接传入null;
第三个参数:一个 ContentValues对象,提供了一系列的 put() 方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。
xml:
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
kt:
1 | class MainActivity : AppCompatActivity() { |
7.4.4 更新数据
SQLiteDatabase中的update()方法。
接收4个参数:
第一个参数:表名;
第二个参数:ContentValues对象,要把更新数据在这里组装进去;
第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认会更新所有行。
xml文件:
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | class MainActivity : AppCompatActivity() { |
这里使用了第三、第四个参数来指定具体更新哪几行。第三个参数对应的是SQL语句的 where 部分,表示更新所有name等于?的行,而?是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容,arrayOf() 方法是 Kotlin 提供的一种用于便捷创建数组的内置方法。
7.4.5 删除数据
SQLiteDatabase中提供delete()方法
3个参数:表名;第二、第三个参数用于约束删除某一行或某几行的数据,不指定的话默认会删除所有行。
1 | class MainActivity : AppCompatActivity() { |
7.4.6 查询数据
QLiteDatabase中提供了query()方法用于对数据查询。
这个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。
7个参数:
第一个参数:表名。
第二个参数:用于指定去查询哪几列,如果不指定则默认查询所有列。
第三、第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据。
第五个参数用于指定需要去group by的列,不指定则表示不对查询结果进行group by操作。
第六个参数用于对group by之后的数据进行进一步的过滤,不指定则表示不进行过滤。
第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。
调用query()方法后会返回一个Cursor对象,查询到的所有数据都将从这个对象中取出。
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操作数据库
7.5 SQLite数据库的最佳实践
事务的特性可以保证让一系列的操作要么全部完成,
要么一个都不会完成。
7.5.1 使用事务
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | class MainActivity : AppCompatActivity() { |
7.5.2 升级数据库的最佳写法
这里需要为每一个版本号赋予其所对应的数据库变动,然后在onUpgrade()方法
中对当前数据库的版本号进行判断,再执行相应的改变就可以了。
1 | class MyDatabaseHelper(val context: Context, name: String, version: Int): |
新的需求:需要在Book 表中添加一个category_id字段。
1 | class MyDatabaseHelper(val context: Context, name: String, version: Int): |
为什么设置多层if< 判断?
服务App跨版本升级
八,ContentProvider
Android中不同APP之间数据共享的一种手段
8.1 运行时权限
Android目前权限分为两类
普通权限:在AndroidManifest.xml文件中添加权限声明后就可以直接使用(系统自动)
危险权限:特殊对待运行时权限处理
到 Android 10 系统为止所有的危险权限
用户一旦同意了某个权限申请之后,同组的其他权限也会被系统自动授权。
但Android系统随时有可能调整权限的分组。 ,利用这个小Bug貌似没啥用~
8.2 在程序运行时申请权限的演示
Android 6.0开始在使用危险权限时必须进行运行时权限处理
新方法简介:
ContextCompat.checkSelfPermission() 判断是否拿到某权限
接收两个参数:第一个Context,第二个具体的权限名。比如Manifest.permission.CALL_PHONE(打电话)
ActivityCompat.requestPermissions() 向用户申请授权。
接收3个参数:第一个参数要求是Activity的实例(在哪一个Activity中申请权限);第二个参数是一个String数组,里面是申请的权限名;第三个参数是请求码,要求是唯一值
1 | class MainActivity : AppCompatActivity() { |
运行时异常给了一次就默认一直有了,要取消去设置看
这里当初出了一个玄学Bug,苦寻无果。最后是过了很久换了一个项目才跑起来的。
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/table2val uri = Uri.parse(“content://com.example.app.provider/table1”)
只需要调用Uri.parse()方法,就可以将内容URI字符串解析成Uri对象,Uri对象才能当参数传递
query方法详解
具体代码演示:
1 | //query |
8.3.2 读取系统联系人
xml文件
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
kt文件:
1 | package com.lmc.android28 |
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
貌似我的包名当初写错啦?
8.4 创建自己的 ContentProvider
8.4.1 理论知识
想要自己的APP的数据也可以让别的APP共享,可以新建一个类去继承 ContentProvider ,然后实现他的6个抽象方法。
1 | class MyProvider : ContentProvider() { |
URI
标准:content://com.example.app.provider/table1
这就表示调用方期望访问的是com.example.app这个应用的table1表中的数据。
精准:content://com.example.app.provider/table1/1
这就表示调用方期望访问的是com.example.app这个应用的table1表中id为1的数据
1 | class MyProvider : ContentProvider() { |
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 | class MyProvider : ContentProvider() { |
为什么 ContentResolver能保证隐私数据不会泄漏出去呢?
因为所有的增删改查操作都一定要匹配到相应的内容URI格式才能进行,而我们当然不可能向UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问,安全问题也就不存在了。
8.4.2 代码实践
为了方便我们在Android34 来新建ContentProvider,AS会自动做其他工作,很方便的。
DatebaseProvider
跨程序访问时我们不能直接使用Toast
1 | package com.lmc.android34 |
下面是测试案例
首先,把虚拟器上的那个APP卸载重装
然后新建一个项目
xml文件:
1 |
|
kt文件:
1 | package com.lmc.android39 |
九,多媒体
9.1 通知
9.1.1 通知渠道
什么是通知渠道呢?
根据通知的重要等级分为不同类
-
NotificationManager 通知管理
调用 Context 的getSystemService() 方法获取。
getSystemService() 方法接收一个字符串参数(获取系统的哪个服务)1
2val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
... -
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 通知的基本用法
-
创建 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
7val 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对象
-
全局 MainActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class 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)
}
}
}图片可以自己准备也可以用老师的
-
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
18class 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)
}
}
} -
点了通知后,通知图标没有消失
一种是在NotificationCompat.Builder中再连缀一个setAutoCancel()方法,
一种是在被打开页面中显式地调用NotificationManager的cancel()方法将它取消。第一种办法:
1
2
3
4val notification = NotificationCompat.Builder(this, "normal")
...
.setAutoCancel(true)
.build()第二种:
想取消什么通知就传入对应它的 id
1
2
3
4
5
6
7
8
9class 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
- 通知内容文字过长
用 setStyle
代替setContentText
原来
1 | val notification = NotificationCompat.Builder(this, "normal") |
升级
1 | val notification = NotificationCompat.Builder(this, "normal") |
- 显示图片
1 | val notification = NotificationCompat.Builder(this, "normal") |
9.1.3.2 不同重要等级渠道对通知行为具体影响
通知渠道的重要等级越高,发出的通知就越容易获得用户的注意。
(打开王者,他自己会弹出来最新的活动,那个活动就是等级最高的通知)
1 | class MainActivity : AppCompatActivity() { |
如果没有浮窗或者声音:关于Android通知的浮动通知(横幅)不显示的解决方法_IT冰棍的博客-CSDN博客_android 不显示浮动通知
9.2 摄像头&相册
9.2.1 调用摄像头拍照
新建一个项目
xml 中
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
有个 button 和 一个 image 显示拍出来的照片
/我的手机/Android/data/
MainActivity
1 | package com.lmc.android42 |
AndroidManifest.xml
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
右击xml目录→New→File,创建一个file_paths.xml文件
1 |
|
external-path就是用来指定Uri共享路径的,name属性的值可以随便填,path属性的值表示共享的具体路径。这里使用一个单斜线表示将整个SD卡进行共享,当然你也可以仅共享存放output_image.jpg这张图片的路径。
9.2.2 从相册中选择图片
在上面的APP中直接改好了
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | class MainActivity : AppCompatActivity() { |
思考:如果照片像素很高直接加载到内存会直接崩溃的,一般会压缩之后再导进来的。
最终成品贴一下:
1 | package com.lmc.android42 |
9.3 播放多媒体文件
9.3.1 播放音频
MediaPlayer 类
MediaPlayer 的工作流程
- 创建MediaPlayer 对象
- 调 setDataSource()方法设置音频文件的路径
- 调用 prepare() 方法使 MediaPlayer 进入准备状态
- 接下来调用 start() 方法就可以开始播放音频
- 调用pause()方法就会暂停播放
- 调用 reset() 方法就会停止播放。
新建一个项目
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
main 下创建一个assets目录,并在这个目录下存放任意文件和子目录,这些文件和子目录在项目打包时会一并被打包到安装文件中,然后我们在程序中就可以借助AssetManager这个类提供的接口对assets目录下的文件进行读取。
右击app/src/main→New→Directory,在弹出的对话框中输入“assets”,目录就创建好了,放一个mp3文件
1 | class MainActivity : AppCompatActivity() { |
9.3.2 播放视频
VideoView 工具类
VideoView不支持直接播放assets目录下的视频资源
res目录下允许我们再创建一个raw目录,音频、视频之类的资源文件可以放在这里,VideoView是可以直接播放这个目录下的视频资源的。 mp4
新建一个项目
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | class MainActivity : AppCompatActivity() { |
十,Service
Service并不是运行在一个独立的进程当中的,而是依赖于创建Service时所在的 APP 进程。 当某个应用程序进程被杀掉时,所有依赖于该进程的Service也会停止运行。
10.1 Android 多线程编程
10.1.1 线程基本用法
1 | thread { |
演变和其他版本可以看参考教材《第一行代码 第三版》
10.1.2 在子线程中更新 UI
Android的UI也是线程不安全的。
Android不允许在子线程中操作UI
解决办法:异步消息处理机制
1 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | package com.lmc.android45 |
10.1.3 解析异步消息处理机制
组成:Message、Handler、MessageQueue 和 Looper。
- Message
线程之间传递的消息,内部携带少量的信息,用于在不同线程之间传递数据
Message的what字段,除此之外还可以使用arg1和arg2字段来携带一些整型数据,使用obj字段携带一个Object对象。 - Handler
Handler顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用Handler的sendMessage()方法、post()方法等,而发出的消息经过一系列地辗转处理后,最终会传递到Handler的handleMessage()方法中。 - MessageQueue
MessageQueue 是消息队列的意思,它主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一 MessageQueue对象。 - Looper
Looper 是每个线程中的 MessageQueue 的管家,调用 Looper 的 loop() 方法后,就会进入一个无限循环当中,然后每当发现MessageQueue中存在一条消息时,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中只会有一个Looper对象。
异步消息处理流程:首先需要在主线程当中创建一个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。当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
这个抽象类需要重写的方法
-
onPreExecute()
在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。 -
doInBackground(Params…)
这个方法中的所有代码都会在 子线程 中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成,就可以通过return语句将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是Unit,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用 publishProgress (Progress…) 方法来完成。 -
onProgressUpdate(Progress…)
当在后台任务中调用了publishProgress(Progress…)方法后,onProgressUpdate (Progress…)方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。
-
onPostExecute(Result)
当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据进行一些UI操作,比如说提醒任务执行的结果,以及关闭进度条对话框等。
完整 Async Task
1 | class DownloadTask : AsyncTask<Unit, Int, Boolean>() { |
简单来说,使用AsyncTask的诀窍就是,在doInBackground()方法中执行具体的耗时任务,在onProgressUpdate()方法中进行UI操作,在onPostExecute()方法中执行一些任务的收尾工作。
你也可以给 execute() 方法传入任意数量的参数,这些参数将会传递到 DownloadTask 的 doInBackground() 方法当中。
10.2 Service 基本用法
新建项目
10.2.1 定义一个 Service
1 | package com.lmc.android46 |
其中 onCreate() 方法会在 Service 创建的时候调用,onStartCommand() 方法会在每次Service 启动的时候调用,onDestroy() 方法会在 Service 销毁的时候调用。
10.2.2 启动和停止 Service
1 | package com.lmc.android46 |
Service 自我停止运行 = Service 内部调用stopSelf()
APP在前台可见时候 Service 才能保证稳定运行,一旦应用进入后台之后,Service随时都有可能被系统回收,解决办法可以使用前台Service或者WorkManager
10.2.3 Activity和Service进行通信
利用 onBind 方法
1 | class MyService : Service() { |
xml
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
为了在Activity中控制Service1,可以把两者绑定起来
1 | class MainActivity : AppCompatActivity() { |
Service在整个应用程序范围内都是通用的,MyService可以和多个Activity进行绑定,而且在绑定完成后,它们都可以获取相同的DownloadBinder实例。
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 | class MyService : Service() { |
另外,从Android 9.0系统开始,使用前台Service必须在AndroidManifest.xml文件中进行权限声明才行
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
10.4.2 使用 IntentService
Service中的代码都是默认运行在主线程当中的,如果直接在Service里处理一些耗时的逻辑,就很容易出现ANR(Application Not Responding)的情况。所以这个时候就需要用到 Android 多线程编程的技术了,我们应该在Service的每个具体的方法里开启一个子线程,然后在这里处理那些耗时的逻辑。
本来一套逻辑
1 | class MyService : Service() { |
为了可以简单地创建一个异步的、会自动停止的Service,Android专门提供了一个 IntentService 类
IntentService
1 | class MyIntentService : IntentService("MyIntentService") {//构造字符串随意好了 |
然后要在子类中实现 onHandleIntent() 这个抽象方法,这个方法中可以处理一些耗时的逻辑,而不用担心ANR的问题,因为这个方法已经是在子线程中运行的了。这里为了证实一下,我们在 onHandleIntent() 方法中打印了当前线程名。另外,根据 IntentService 的特性,这个 Service 在运行结束后应该是会自动停止的,所以我们又重写了 onDestroy() 方法,在这里也打印了一行日志,以证实 Service 是不是停止了。
验证工作:
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | class MainActivity : AppCompatActivity() { |
注意,Service 有没有在AndroidManifest 中注册(手动自动)
十一,网络
11.1 WebView
一个WebView控件,借助它我们就可以在自己的应用程序里嵌入一个浏览器,从而非常轻松地展示各种各样的网页。
新建一个项目
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | package com.lmc.android50 |
访问网络是需要申请权限的
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
11.2 手动使用 HTTP 访问网络
客户端向服务器发出一条HTTP请求,服务器收到请求之后会返回一些数据给客户端,然后客户端再对这些数据进行解析和处理就可以了。
11.2.1 使用HttpURLConnection
Android上发送HTTP请求
1 | val url = URL("https://www.baidu.com") |
- 首先需要获取HttpURLConnection的实例,一般只需创建一个URL对象,并传入目标的网络地址,然后调用一下openConnection()方法即可
- 在得到了HttpURLConnection的实例之后,我们可以设置一下HTTP请求所使用的方法。常用的方法主要有两个:GET和POST。GET表示希望从服务器那里获取数据,而POST则表示希望提交数据给服务器。
- 接下来就可以进行一些自由的定制了,比如设置连接超时、读取超时的毫秒数,以及服务器希望得到的一些消息头等。
- 之后再调用getInputStream()方法就可以获取到服务器返回的输入流了,剩下的任务就是对输入流进行读取
- 最后可以调用disconnect()方法将这个HTTP连接关闭
ScrollView。它是用来做什么的呢?由于手机屏幕的空间一般比较小,有些时候过多的内容一屏是显示不下的,借助ScrollView控件,我们就可以以滚动的形式查看屏幕外的内容。
一个手动访问服务器的小演示
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | class MainActivity : AppCompatActivity() { |
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
向服务器提交数据
只需要将HTTP请求的方法改成POST,并在获取输入流之前把要提交的数据写出即可。注意,每条数据都要以键值对的形式存在,数据与数据之间用“&”符号隔开。
1 | connection.requestMethod = "POST" |
11.2.2 使用OkHttp
- app/build.gradle 加上依赖包
1 | dependencies { |
- 首先需要创建一个OkHttpClient
- 创建一个Request对象
- 调用OkHttpClient的newCall()方法来创建一个Call对象,并调用它的execute()方法
来发送请求并获取服务器返回的数据 - 得到返回的具体内容
GET:
1 | val client = OkHttpClient() |
POST:
1 | val client = OkHttpClient() |
用这个 OkHttp 优化之前用 HttpURLConnection 的哪一个 Android50二
1 | class MainActivity : AppCompatActivity() { |
11.3 解析XML格式数据
辣个 Apache 搭建说一下,《第一行代码 第三版》上说的,这边试过是完全用不了的。我写了一个博客实测是可行的,各种遇到的问题也在里面收集了解决方案。
后面看情况上传公众号或者B站专栏。
11.3.0 Apache 准备
这里一会我把来链接贴出来:
进入C:\Apache\htdocs目录下,在这里新建一个名为get_data.xml的文件,然后编辑
这个文件,并保存。
1 | <apps> |
然后再在浏览器中访问localhost:8088/get_data.xml
空白处右击选择查看页面源代码后
11.3.1 Pull解析方式
说一下下面第29行代码的注意:这个地方看视频怎末说的。是个大坑,稍不注意就会出错。
1 | package com.lmc.android50 |
那么为了能让程序使用HTTP,我们还要进行如下配置才可以。在res目录下右击xml目录→New→File,创建一个network_config.xml文件。然后修改network_config.xml文件中的内容,如下所示:
1 | <?xml version="1.0" encoding="utf-8"?> |
这段配置文件的意思就是允许我们以明文的方式在网络上传输数据,而HTTP使用的就是明文传输方式。
1 |
|
就加了一个 networkSecurityConfig
结果:
11.2.3.2 SAX解析方式
要使用SAX解析,通常情况下我们会新建一个类继承自DefaultHandler,并重写父类的5个
方法
1 | class MyHandler : DefaultHandler() { |
其中,startElement()、characters()和endElement()这3个方法是有参数的,从XML中解析出的数据就会以参数的形式传入这些方法中。需要注意的是,在获取节点中的内容时,characters()方法可能会被调用多次,一些换行符也被当作内容解析出来,我们需要针对这种情况在代码中做好控制。
1 | class ContentHandler : DefaultHandler() { |
11.2.3.3 DOW解析方式
自己有兴趣再去了解
11.4 解析 JSON 格式数据
比起XML,JSON的主要优势在于它的体积更小,在网络上传输的时候更省流量。但缺
点在于,它的语义性较差,看起来不如XML直观。
C:\Apache24\htdocs目录中新建一个get_data.json的文件
1 | [{"id":"5","version":"5.5","name":"Clash of Clans"}, |
11.4.1 使用JSONObject
官方的
1 | class MainActivity : AppCompatActivity() { |
首先将HTTP请求的地址改成http://10.0.2.2/get_data.json,然后在得到服务器返回的数据后调用parseJSONWithJSONObject()方法来解析数据。可以看到,解析JSON的代码真的非常简单,由于我们在服务器中定义的是一个JSON数组,因此这里首先将服务器返回的数据传入一个JSONArray对象中。然后循环遍历这个JSONArray,从中取出的每一个元素都是一个JSONObject对象,每个JSONObject对象中又会包含id、name和version这些数据。接下来只需要调用getString()方法将这些数据取出,并打印出来即可。
11.4.2 使用GSON
谷歌开源库
1 | dependencies { |
实操
1 | //新增一个App类 |
还有 Jackson,FastJSON 等
11.5 网络请求回调的实现方式
通常情况下我们应该将这些通用的网络操作提取到一个公共的类里,并提供一个通用方法,当想要发起网络请求的时候,只需简单地调用一下这个方法即可。
来用 HttpURLConnection
1 | //下面是一个不正确的临时演示代码 |
看起来没有问题,但实际是不可行的。网络请求通常属于耗时操作,而sendHttpRequest()方法的内部并没有开启线程,这样就有可能导致在调用sendHttpRequest()方法的时候主线程被阻塞。
那在sendHttpRequest()方法内部开启一个线程?如果我们在sendHttpRequest()方法中开启一个线程来发起HTTP请求,服务器响应的数据是无法进行返回的。这是由于所有的耗时逻辑都是在子线程里进行的,sendHttpRequest()方法会在服务器还没来得及响应的时候就执行结束了,当然也就无法返回响应的数据了。
解决方法-》编程语言的回调机制。
1 | //这个是原始版本的OK的代码演示 |
注意,子线程中是无法通过 return 语句返回数据的,因此这里我们将服务器响应的数据传入了 HttpCallbackListener 的 onFinish() 方法中,如果出现了异常,就将异常原因传入 onError() 方法
如此一来,我们就巧妙地利用回调机制将响应数据成功返回给调用方了。
用 OkHttp 会简单一点
1 | object HttpUtil { |
需要注意的是,不管是使用HttpURLConnection还是OkHttp,最终的回调接口都还是
在子线程中运行的,因此我们不可以在这里执行任何的UI操作,除非借助runOnUiThread()
方法来进行线程转换
11.6 最好用的网络库:Retrofit
新建一个项目
11.6.1 基本用法
加上依赖
1 | dependencies { |
由于Retrofit是基于OkHttp开发的,因此添加上述第一条依赖会自动将Retrofit、OkHttp和Okio这几个库一起下载,我们无须再手动引入OkHttp库。另外,Retrofit还会将服务器返回的JSON数据自动解析成对象,因此上述第二条依赖就是一个Retrofit的转换库,它是借助GSON来解析JSON数据的,所以会自动将GSON库一起下载下来,这样我们也不用手动引入GSON库了。除了GSON之外,Retrofit还支持各种其他主流的JSON解析库,包括Jackson、Moshi等,不过毫无疑问GSON是最常用的 。
1 | class App(val id: String, val name: String, val version: String) |
网络安全配置:复制 network_config.xml 文件到 Retrofit项目当中,然后修改AndroidManifest.xml中的代码
设置了允许使用明文的方式来进行网络请求,同时声明了网络权限
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
11.6.2 处理复杂的接口地址类型
上一个小节,我们向一个非常简单的服务器接口地址发送请求。但实际中这个是很复杂的。
准备:
1 | //1.最简单的静态服务器接口地址 |
HTTP类型:GET请求用于从服务器获取数据,POST请求用于向服务器提交数据,PUT和PATCH请求用于修改服务器上的数据,DELETE请求用于删除服务器上的数据。
而Retrofit对所有常用的HTTP请求类型都进行了支持,使用@GET、@POST、@PUT、@PATCH、@DELETE注解,就可以让Retrofit发出相应类型的请求了
比如:
1 | //1. 删除 |
这里我们在createData()方法中声明了一个Data类型的参数,并给它加上了@Body注解。这样当Retrofit发出POST请求时,就会自动将Data对象中的数据转换成JSON格
式的文本,并放到HTTP请求的body部分,服务器在收到请求之后只需要从body中将这部分数据解析出来即可。这种写法同样也可以用来给PUT、PATCH、DELETE类型的请求提交数据。
服务器接口要求HTTP请求的header中指定参数,例如:
1 | //静态header声明 |
11.6.3 Retrofit构建器的最佳写法
开始的写法
1 | val retrofit = Retrofit.Builder() |
1 | object ServiceCreator {//单例类 |
十二,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 | <?xml version="1.0" encoding="utf-8"?> |
为了能够兼容老系统我们使用xmlns:app指定了一个新的命名空间(其实人家一直都有)
制定了 xmlns:app 后面才能 app:啥子
不然就只能 android:啥子
android:layout_height="?attr/actionBarSize"
:高度设置为actionBar的高度
最后两行设置主题的
:因为APP是浅色,然后这个Toolbar也会是浅色,那它上面的字啥的就会是深色,有点丑。所以我们单独设置这个Toolbar为深色,这样上面的labal就是以前的白色了。但是这样到时候Toolbar上弹出的小菜单(就是那三个点)就也是白色了,就在APP的浅色中看不见。所以又单独给弹出的菜单设置为浅色主题,这样他上面的菜单字就是黑的,又可以看见了。
挺有意思的,不信你去改改?
给你看下没这个的后果:
- 啥都不加:
- 就加第一行:
12.1.1.3 改MainActivity
1 | class MainActivity : AppCompatActivity() { |
跑一下,虽然看起来没有啥变化但是它已经从 ActionBar 变身为 Toolbar(虽然在你AS上跑本来就是Toolbar)
下面这个没用下次可以删了
1 | package com.lmc.android121 |
12.1.2 Toolbar的特有功能
12.1.2.1 改Toolbar(标题栏)上的 slogan(标语)
其实这个就是APP名
来到 AndroidManifest.xml
,在".MainActivity"
1 |
|
android:label属性,用于指定在Toolbar中显示的文字内容
12.1.2.2 加点action按钮
把图片文件拉过来放在了drawable-xxhdpi目录,
右击res目录→New→Directory,创建一个menu文件夹。
然后右击menu文件夹→New→Menu resource file,创建一个toolbar.xml文件
文件中的代码如下:
1 |
|
app:showAsAction来指定按钮的显示位置 ,
always表示永远显示在Toolbar中,如果屏幕空间不够则不显示;
ifRoom表示屏幕空间足够的情况下显示在Toolbar中,不够的话就显示在菜单当中;
never则表示永远显示在菜单当中。
注意,Toolbar中的action按钮只会显示图标,菜单中的action按钮只会显示文字。
然后修改MainActivity中的代码:
1 | package com.lmc.android121 |
意外:
12.2.3 Toolbar最后成果图
12.2 滑动菜单
就是把一些菜单选项隐藏起来,而不是放在主屏幕上,然后可以通过滑动现实出来。就是QQ的辣个个人界面一样。
12.2.1 DrawerLayout(抽屉布局)
抽屉布局:第一个子控件是主屏幕中显示的内容,第二个子控件是滑动菜单中显示的内容。(实现滑动菜单的最外面保障)
12.2.1.1 换上抽屉布局
改activity_main.xml
1 | <?xml version="1.0" encoding="utf-8"?> |
解释一哈:
我们把最外面替换成了抽屉布局,然后主屏幕控件是一个FrameLayout,里面还是我们的 Toolbar ;第二个滑动菜单中显示的内容是一个 TextView 。
关键注意:第二个控件一定要加
layout_gravity
,这个是决定到时候滑动菜单从哪个地方弹出来。(而且这个地方,他没有自动补全,你只能复制我的或者一个字母一个字母的打出来)
12.2.1.2 加滑动菜单的导航按钮
为了多一个点击弹出来菜单按钮,因为有些同学不知道这个地方会有菜单。
在drawable-xxhdpi目录下准备一张图标,
1 | class MainActivity : AppCompatActivity() { |
然后就差不多了:
12.2.2 NavigationView(导航页面)
12.2.2.1 引入圆角库
1 | dependencies { |
这里添加了一行依赖关系:是一个开源项目CircleImageView,实现图片圆形化的功能
因为现在AS一般自带MetialDesign,所以不用自己加
12.2.2.2 换主题
将 res/values/themes.xml 文件中 AppTheme 的 parent 主题改成Theme.MaterialComponents.Light.NoActionBar,否则在使用接下来的一些控件时可能会遇到崩溃问题。
12.2.2.3 准备menu
素材图片放在drawablexxhdpi目录下,
右击menu文件夹→New→Menu resource file,创建一个nav_menu.xml文件,并编写如下代码:
1 |
|
12.2.2.4 准备 headerLayout
图片还是放在老地方:drawablexxhdpi
右击layout文件夹→New→Layout resource file,创建一个nav_header.xml文件。
修改其中的代码,如下所示:
1 |
|
然后修改activity_main.xml中的代码,如下所示:
1 |
|
菜单点击事件:
1 | class MainActivity : AppCompatActivity() { |
12.3 悬浮按钮和可交互提示
12.3.1 FloatingActionButton
首先放一张 good.png到drawable-xxhdpi目录下。然后修改activity_main.xml中的代码 ,在FrameLayout中加FloatingActionButton
1 |
|
其实这个东西的悬浮高度也可以指定:就是最后一行(其实感觉不大)
1 | <com.google.android.material.floatingactionbutton.FloatingActionButton |
给这个 FloatingActionButton 加点击按钮,MainActivity 中
1 | class MainActivity : AppCompatActivity() { |
12.3.2 Snackbar
比Toast更厉害的提示工具,修改MainActivity中的代码:
1 | class MainActivity : AppCompatActivity() { |
(OS:下面这个图没换)
12.3.3 CoordinatorLayout(协调布局)
刚才辣个,Snakebar 遮住了我们的FAB,怎么处理。
CoordinatorLayout可以说是一个加强版的FrameLayout,事实上,CoordinatorLayout 可以监听其所有子控件的各种事件,并自动帮助我们做出最为合理的响应。举个简单的例子,刚才弹出的 Snackbar 提示将悬浮按钮遮挡住了,而如果我们能让 CoordinatorLayout 监听到 Snackbar 的弹出事件,那么它会自动将内部的 FloatingActionButton 向上偏移,从而确保不会被Snackbar遮挡。
至于CoordinatorLayout的使用:将原来的FrameLayout替换一下就可以了。修改activity_main.xml中的代码,如下所示:
1 |
|
Snakbar是FloatingActionButton的儿子,座椅也会被协调布局管着拉。
一个字,丝滑流畅。
12.4 卡片式布局
12.4.1 MaterialCardView
MaterialCardView也是一个FrameLayout,只是额外提供了圆角和阴影等效果,看上去会有立体的感觉。
1 | <com.google.android.material.card.MaterialCardView |
我们准备在屏幕中间放一些水果,要用RecyclerView
首先来加一个依赖:app/build.gradle文件
1 | dependencies { |
上述声明的第二行是添加了Glide库的依赖。Glide是一个图片加载库
修改activity_main.xml中的代码:
1 |
|
写个实体类Fruit
1 | package com.lmc.android121 |
为RecyclerView的子项指定一个我们自定义的布局,在layout目录下新建
fruit_item.xml
1 |
|
这里使用了MaterialCardView来作为子项的最外层布局,从而使得RecyclerView中的每个元素都是在卡片当中的。由于MaterialCardView是一个FrameLayout,因此它没有什么方便的定位方式,这里我们只好在MaterialCardView中再嵌套一个LinearLayout,然后在LinearLayout中放置具体的内容。
scaleType属性,这个属性可以指定图片的缩放模式.centerCrop模式,它可以让图片保持原有比例填充满ImageView,并将超出屏幕的部分裁剪掉。
为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder
1 | class FruitAdapter(val context: Context, val fruitList: List<Fruit>) : |
首先调用Glide.with()方法并传入一个Context、Activity或Fragment参数,然后调用load()方法加载图片,可以是一个URL地址,也可以是一个本地路径,或者是一个资源id,最后调用into()方法将图片设置到具体某一个ImageView中就可以了。
最后修改MainActivity中的代码
1 | class MainActivity : AppCompatActivity() { |
我们的Toolbar怎么不见了!仔细观察一下原来是被RecyclerView给挡住了
12.4.2 AppBarLayout
为啥子被遮住了:RecyclerView和Toolbar都是放置在CoordinatorLayout中的,而前面已经说过,CoordinatorLayout就是一个加强版的FrameLayout,那么FrameLayout中的所有控件在不进行明确定位的情况下,默认都会摆放在布局的左上角,从而产生了遮挡的现象。
AppBarLayout。AppBarLayout实际上是一个垂直方向的LinearLayout
第一步将Toolbar嵌套到AppBarLayout中,第二步给RecyclerView指定一个布局行为。修改activity_main.xml中的代码。
1 | <androidx.drawerlayout.widget.DrawerLayout |
当AppBarLayout接收到滚动事件的时候,它内部的子控件其实是可以指定如何去响应这些事件的,通过app:layout_scrollFlags属性就能实现。修改activity_main.xml中的代码。
1 | <androidx.drawerlayout.widget.DrawerLayout |
就加了那一行拉
这里在Toolbar中添加了一个app:layout_scrollFlags属性,并将这个属性的值指定成了scroll|enterAlways|snap。其中,scroll表示当RecyclerView向上滚动的时候,Toolbar会跟着一起向上滚动并实现隐藏;enterAlways表示当RecyclerView向下滚动的时候,Toolbar会跟着一起向下滚动并重新显示;snap表示当Toolbar还没有完全隐藏或显示的时候,会根据当前滚动的距离,自动选择是隐藏还是显示。
向下拉就会隐藏。上拉就会显示顶部的辣个
12.5 下拉刷新
SwipeRefreshLayout就是用于实现下拉刷新功能的核心类,我们把想要实现下拉刷新功能的控件放置到SwipeRefreshLayout中,就可以迅速让这个控件支持下拉刷新。
添加插件:
1 | dependencies { |
修改 activity_main.xml
1 |
|
我们在 RecyclerView 的外面又嵌套了一层 SwipeRefreshLayout,这样
RecyclerView 就自动拥有下拉刷新功能了。另外需要注意,由于 RecyclerView 现在变成了 SwipeRefreshLayout 的子控件,因此之前使用 app:layout_behavior 声明的布局行为现在也要移到 SwipeRefreshLayout 中才行
在代码中处理具体的刷新逻辑才行。修改MainActivity:
1 | class MainActivity : AppCompatActivity() { |
12.6 可折叠式标题栏
12.6.1 CollapsingToolbarLayout
一个作用于Toolbar基础之上的布局
干啥的?让Toolbar更加强大
注意事项:
- CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为AppBarLayout的直接子布局来使用。而AppBarLayout又必须是CoordinatorLayout的子布局
12.6.1.1 具体显示的界面布局
一个额外的Activity作为水果的详情展示界面
→New→Activity→Empty Activity,创建一个FruitActivity,并将布局名指定成 activity_fruit.xml ,然后我们开始编写水果详情展示界面的布局。
1 | <androidx.coordinatorlayout.widget.CoordinatorLayout |
app:layout_collapseMode比较陌生。它用于指定当前控件在CollapsingToolbarLayout折叠过程中的折叠模式,其中Toolbar指定成pin,表示在折叠的过程中位置始终保持不变,ImageView指定成parallax,表示会在折叠的过程中产生一定的错位偏移
1 |
|
12.6.2.1 逻辑代码
修改FruitActivity中的代码
1 | class FruitActivity : AppCompatActivity() { |
修改FruitAdapter中的代码
1 | class FruitAdapter(val context: Context, val fruitList: List<Fruit>) : |
12.6.2 充分利用系统状态栏空间
给你想要在系统状态栏显示的控件以及父类控件全部就加上android:fitsSystemWindows属性指定成true
1 |
|
然后把系统状态栏改为透明:
1 | <resources> |
然后去AndroidManifest.xml中,让FruitActivity用这个主题:
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
十三,JetPack
开发组件工具箱,帮助写出更加简洁的代码,并简化开发过程。
有很好的兼容性。
我们这个章节主要研究基础架构组件。
分类图:
13.1 ViewModel
发现的问题:Activity要负责处理逻辑,控制UI,处理网络请求……任务太重
ViewModel:分担Activity一部分工作,==专门放和界面相关数据==
界面上能看到的数据,相关变量都放ViewModel中。因为屏幕旋转时Activity重新创建,数据会丢失,但是ViewModel不会。
ViewModel生命周期:
13.1.1 基本用法
添加依赖
1 | dependencies { |
编程规范:给每一个 Activity 和 Fragment 都创建一个对应的 ViewModel
新建一个MainViewModel
1
2
3
4
5
6
7 package com.lmc.android1301
import androidx.lifecycle.ViewModel
//所有与界面相关的数据都放在ViewModel中
class MainViewModel:ViewModel() {
var counter = 0
}改xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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>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()
}
}
解释的点:
不能直接在onCreate里面创建ViewModel的实例,因为这样他就随着Activity的创建销毁而变化了。我们一般通过ViewModelProvider来获取。下面是语法:
ViewModelProvider(<你的Activity或Fragment实例>).get(<你的ViewModel>::class.java)
结果:
横过来也没有丢失数据:
13.1.2 向 ViewModel 传递参数
小优化:推出程序重新打开数据不丢失
-
MainViewModel
1
2
3
4
5
6
7
8package com.lmc.android1301
import androidx.lifecycle.ViewModel
//所有与界面相关的数据都放在ViewModel中
class MainViewModel(countReserved: Int) : ViewModel() {
var counter = countReserved
} -
新建一个MainViewModelFactory类,实现ViewModelProvider.Factory接口
1
2
3
4
5
6
7
8
9
10
11package 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的生命周期无关
}
} -
一个清零按钮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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
42package 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中编写大量的逻辑处理。
-
新建一个MyObserver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package com.lmc.android1301
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
class MyObserver:LifecycleObserver {
//注解用来让这个行为发生时候调用这个方法
fun activityStart(){
Log.d("MyObserver","activityStart")
}
fun activityStop(){
Log.d("MyObserver","activityStop")
}
} -
怎么让MyObserver 感知到Activity的生命周期变化
1
lifecycleOwner.lifecycle.addObserver(MyObserver())
然后我们的Activity和Fragment都是lifecycleOwner
1
2
3
4
5
6
7
8
9
10class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
lifecycle.addObserver(MyObserver())//加一句这个就可以了
}
...
} -
主动获知当前的生命周期状态
1
2
3
4
5
6
7class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver {
...
}
lifecycle.addObserver(MyObserver(this.lifecycle))
lifecycle.currentState//获取当前生命周期的状态
//这个地方有坑,注意看视频演示。
13.3 LiveDate
LiveData是Jetpack提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。和ViewModel很贴近。
出现的问题:我们刚刚写的那个计数器的功能只能在单线程中工作,ru过ViewMode中开了新的线程去执行耗时任务,那我们点击加一按钮之后就得到的还是之前的数据;
解决办法:将计数器的计数用LiveData包装,然后在Activity中去观察它,这样就可以主动的将数据变化通知给Activity
13.3.1 基本用法
- MainViewModel :
1 | package com.lmc.android1301 |
-
MainActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class 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)
}
}
} -
优化:
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
28package 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对象。
-
新建一个 Repository 单例类
1
2
3
4
5
6
7
8
9
10
11
12
13package 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
}
} -
MainViewModel
1
2
3
4
5
6class MainViewModel(countReserved: Int) : ViewModel() {
...
fun getUser(userId: String): LiveData<User> {
return Repository.getUser(userId)www.blogss.cn
}
} -
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对象就可以了。 -
修改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> -
修改MainActivity 代码
1 | class MainActivity : AppCompatActivity() { |
这里
-
ViewModel中某个获取数据的方法有可能是没有参数的,这个时
候代码应该怎么写呢?1
2
3
4
5
6
7
8
9class MyViewModel : ViewModel() {
private val refreshLiveData = MutableLiveData<Any?>()
val refreshResult = Transformations.switchMap(refreshLiveData) {
Repository.refresh() // 假设Repository中已经定义了refresh()方法
}
fun refresh() {
refreshLiveData.value = refreshLiveData.value//触发数据变化事件
}
}
一些小细节:
- LiveData与ViewModel结合在一起使用时候,底层都是Lifecycles组件作为Activity和ViewModel通信桥梁。
- 由于要减少性能消耗,当Activity处于不可见状态的时候(比如手机息屏,或者被其他的Activity遮挡),如果LiveData中的数据发生了变化,是不会通知给观察者的。只有当Activity重新恢复可见状态时,才会将数据通知给观察者,而LiveData之所以能够实现这种细节的优化,依靠的还是Lifecycles组件。
- 如果在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整体结构
- Entity。封装实际数据得实体类。每个实体类都会在数据库中对饮一张表,表中得列就是根据实体类的字段自动生成;
- Dao。数据访问对象,在这里对数据库的各项操作进行封装。这样逻辑层就不用和底层数据库打交道直接和Dao层交互;
- Database。定义数据库中的关键信息,数据库版本号,包含的实体类以及提供Dao层访问实例。
13.4.1.2 Room使用步骤
- 依赖引入:
1 | plugins { |
解释一下:Room会根据我们在项目中声明的注解来动态生成代码,所以要用kapt引入Room的编译时注解库,而启动编译时注解功能要先添加kotlin-kapt插件。(kotlin项目中使用这个,如果是Java项目那就只用 annotationProcessor,书上的用不了,只有这样才能用。)
-
改User成Entity
1
2
3
4
5
6
7
8
9
10
11package com.lmc.android1301
import androidx.room.Entity
import androidx.room.PrimaryKey
data class User(var firstName: String, var lastName: String, var age: Int) {
var id: Long = 0
} -
定义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.*
interface UserDao {
fun insertUser(user: User): Long
fun updateUser(newsUser: User)
fun deleteUser(user: User)
//从数据库中查询数据
fun loadAllUsers(): List<User>
fun loadUsersOlderThan(age: Int): List<User>
//使用非实体类参数来增删改数据(这时候只能用Query注解)
fun deleteUserByLastName(lastName: String): Int
} -
定义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
25package com.lmc.android1301
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
//多个实体类之间用逗号隔开
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao//提供相应的抽象方法,用于获取之前编写的Dao的实例
companion object {//这里是直接用了一个单例模式
private var instance: AppDatabase? = null
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功能测试
-
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> -
修改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
35class 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
3Room.databaseBuilder(context.applicationContext, AppDatabase::class.java,"app_database")
.allowMainThreadQueries()
.build() -
结果:
添加User后
更新后
执行删除后
13.4.2 Room 的数据库升级
Room的数据库升级方面很垃圾,LitePal这个还行有兴趣了解一下。
看看Room最简单的怎么用:
1 | //简单粗暴 |
后果:只要数据库升级,Room就将当前的数据库销毁,然后重新创建。
代价:之前数据库中的所有数据都丢失了。
故:这种只能在开发和测试的时候使用。
Room升级数据库的正规写法:
-
新增一个Book实体类
1
2
3
4
5
6
7
8
9
10
11package com.lmc.android1301
import androidx.room.Entity
import androidx.room.PrimaryKey
data class Book(var name: String, var pages: Int) {
var id:Long = 0
} -
BookDao接口
1
2
3
4
5
6
7
8
9
10
11
12
13package com.lmc.android1301
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
interface BookDao {
fun insertBook(book: Book):Long
fun loadAllBooks():List<Book>
} -
修改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//新增一张表的写法
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
}
}
}
} -
只一个表里面加点东西
1
2
3
4
5
6
7
8
9
10
11package com.lmc.android1301
import androidx.room.Entity
import androidx.room.PrimaryKey
//Book表加了作者
data class Book(var name: String, var pages: Int,var author:String) {
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
46package 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
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 | dependencies { |
-
做一个有任务逻辑的后台任务;
新建一个SimpWork类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14package 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()方法来重新执行任
}
} -
写这个后台任务的运行条件和约束请求,并发起后台任务请求;
例如:
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 -
将后台任务请求放在WorkManager的enqueue方法中,系统知道怎么做。
1
WorkManager.getInstance(context).enqueue(request)
-
测试
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
11class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
doWorkBtn.setOnClickListener {
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
WorkManager.getInstance(this).enqueue(request)
}
}
...
}
13.5.2 处理复杂任务
-
让后台任务在指定的延迟时间后运行setInitialDelay()
第二步写后台请求时:
1
2
3val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setInitialDelay(5, TimeUnit.MINUTES)
.build() -
给后台任务+标签
1
2
3
4val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
...
.addTag("simple")
.build()加标签的好处:可以通过标签来取消后台任务请求。
1
2
3
4
5
6
7
8WorkManager.getInstance(this).cancelAllWorkByTag("simple")
//否则就只能用id来取消
WorkManager.getInstance(this).cancelAllWorkById(request.id)
//用id只能取消单个,而标签能取消一个系列
//发疯了取消所有后台任务请求
WorkManager.getInstance(this).cancelAllWork() -
重新执行任务
如果后台任务的doWork()方法中返回了Result.retry(),
那么是可以结合setBackoffCriteria()方法来重新执行任务的1
2
3
4val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
...
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.build()备注:
-
后面两个指定多久之后重新执行,最低10s
-
第一个参数则用于指定如果任务再次执行失败,下次重试的时间应该以什么样的形式延迟。
LINEAR:下次重试时间以线性方式延迟;
EXPONENTIAL:下次延迟时间以指数方式延迟
-
-
监听后台任务执行结果
1
2
3
4
5
6
7
8
9
10WorkManager.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()方法,监听同一标签名下所有后台任务请求的运行结果 -
链式任务
1
2
3
4
5
6
7
8
9val sync = ...//同步后台任务请求
val compress = ...//压缩
val upload = ...//上传
WorkManager.getInstance(this)
.beginWith(sync)//开启一个链式任务
.then(compress)
.then(upload)
.enqueue()
//规则:在前面一个任务执行完毕之后才能执行后面一个
总结:WorkManager在国产手机上不稳定,很容易被杀。别拿去实现什么核心功能。
十四,高级技巧
14.1 全局获取 Context 的技巧
实现方法:定义一个自己的Application类,
这个类每次app启动,系统就会自动对这个类进行初始化。它可以用来便于管理app内一些全局变量的状态信息。
1 | package com.lmc.android1401 |
注解作用:这里本来Context是不能设置为静态变量的,可能会导致内存泄漏。但是我们这里的Context是直接从application中拿出来的,不是Activity不是Service全局只会有这么一个实例,而且整个app的生命周期内都不会回收,所以直接给一个注解说明一下就可以了。
告诉系统以后初始化我们自己做的这个application类,别整自己那套:
1 |
|
就是那个 name
。
中了,以后不管你在项目的哪里,想要context直接MyApplication.context 就阔以了。
14.2 使用 Intent 传递对象
问题:
Intent在传递信息的时候啊,只能规规矩矩地传那些基本的数据类型,比如什么Int,String……啥的,我想传个对象,比如Student怎么整?
两个办法:Serializable和Parcelable
14.2.1 Serializable 方式
关键三字:序列化。
也就是说我们把这个对象序列化了,然后序列后的东西就可以随便传输了。(JavaSE IO有说过)
举例:
1 | //Person实体中 |
有个小问题:就是FirstActivity中序列化前的Person和SecondActivity反序列化出来的Person是同一个对象嘛?
不是,他们是两个不同的对象只是里面的数据相同。HashCode不一样但是value一样。
这种办法因为要将整个数据序列化,所以效率会低一点。
14.2.2 Parcelable 方式
原理:把对象一块块的分解成支持传送的数据类型然后再传。
一般形式:
1 | //Person实体类中 |
太麻烦了,看下面这个:
1 | //Person实体类 |
前提:要传输的所有数据都放在主构造函数里面。
推荐用这种方法
14.3 定制自己的日志工具
安卓自己的日志工具很强,但是还有弱点:比如日志打印的控制。
开发阶段许多日志需要打印,上线之后就把日志屏蔽。(为什么?机密泄露,性能降低)
解决办法:定制一个自己的日志工具
1 | //新建一个LogUtil单例类 |
14.4 调试 Android 程序
-
添加断点。鼠标左击一下,取消就再加一下;
-
开启调试。小虫子点一下;
-
其实这一个用的更多:
比如你做了一个登录界面,用前面的那个,如果你的断点打在靠后的位置,那么前面输入数据的步骤就一卡一卡的。这时候我们就可以用后面这款。
打好断点,然后正常启动,然后输入数据后点击那个奇怪的小虫子,再去点登录。这时候又可以直接debug。
14.5 深色主题
Android系统官方支持夜间模式:深色主题是在Android10.0。
也就是说,之前只能app自己打开关闭深色模式,现在Android系统打开深色模式全部适配了这个功能的APP就都切换成深色模式了。方便太多。
OS:要是用户系统开个深色模式,别的APP都切换成黑的了,救你的APP贼拉刺眼那样很败兴致。
其实吧,现在AS都是自动给你搞了深色模式。不信之前我们写的小demo你手机系统切换到深色模式回去看看?是不是都变黑了。
14.5.1 Force Dark(了解)
原理:系统会分析你的App的每一层View并把他们的颜色自动换成和深色主题更加贴合的颜色。(注意只有原App是浅色才会这样,本来就是深色那还玩个锤子)
-
做个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> -
真是又丑又难看哈哈哈哈哈
14.5.2 DayNight主题
这种就是他会根据系统主题来自动变light还是dark。(AS默认自动的就是原生设计中的这种)
如果是老版本,改唯一的style文件为这样:
1 | <resources> |
虽说还是有点丑,起码好了点。但是吧这样一个DayLight主题,那个头上还是light的没有一整个dark。(这是一个主题文件夹瘸腿得地方)
为啥?因为我们状态栏是直接指定是啥颜色的,这叫硬编码,你不上手改,系统大人也不能擅做主张啊。
==解决方案:==新建一个values-night目录,然后做一个同名theme文件,里面还是用的DayLight主题,但是标题栏就根据不同情况自己指定好了。 (其实这才是AS默认得那个)
小建议:我们要尽量避免给定颜色这种硬编码方式,而去尝试能够根据当前主题自动切换颜色得主题属性。比如下图:
1 | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
想根据浅色主题和深色主题来分别执行不同业务逻辑?
怎么判断当前系统是否为深色模式:
1 | fun isDarkTheme(context: Context): Boolean { |
备注:Kotin中取消了按位运算符,直接换成了英语关键字。(C,爱了。天下苦位运算符久矣。Kotlin揭竿而起!)
and 就是Java的 &
or 就是Java的 |
xor 就是Java的 ^
补充
1. 怎么查看类的继承关系和组成
查看类的继承关系:点类名,然后 Ctrl+H
查看类的组成结构:
2.Kotlin 跟 Java 的关系
xx.kt -> xx.class -> JVM -> 二进制文件
3. Java 是什么语言
编程语言大致可以分为两类:
编译型语言和解释型语言。编译型语言的特点是编译器会将我们编写的源代码一次性地编译成计算机可识别的二进制文件,然后计算机直接执行,像C和C++都属于编译型语言。解释型语言则完全不一样,它有一个解释器,在程序运行时,解释器会一行行地读取我们编写的源代码,然后实时地将这些源代码解释成计算机可识别的二进制数据后再执行,因此解释型语言通常效率会差一些,像Python和JavaScript都属于解释型语言。
- Java是解释性语言
4. 为什么 Koltin 会设计出 val 关键字?
-
建议定义变量,类,方法,一开始都加上 final
-
如果一个类不是专门为继承而设计的,那么就应
该主动将它加上final声明这里你可能会产生疑惑:既然val关键字有这么多的束缚,为什么还要用这个关键字呢?干脆全部用var关键字不就好了。其实Kotlin之所以这样设计,是为了解决 Java 中final关键字没有被合理使用的问题。
在 Java 中,除非你主动在变量前声明了 final 关键字,否则这个变量就是可变的。然而这并不是一件好事,当项目变得越来越复杂,参与开发的人越来越多时,你永远不知道一个可变的变量会在什么时候被谁给修改了,即使它原本不应该被修改,这就经常会导致出现一些很难排查的问题。因此,一个好的编程习惯是,除非一个变量明确允许被修改,否则都应该给它加上final关键字。
但是,不是每个人都能养成这种良好的编程习惯。我相信至少有90%的Java程序员没有主动在变量前加上final关键字的意识,仅仅因为Java对此是不强制的。因此,Kotlin 在设计的时候就采用了和Java完全不同的方式,提供了val和var这两个关键字,必须由开发者主动声明该变量是可变的还是不可变的。
那么我们应该什么时候使用val,什么时候使用var呢?这里我告诉你一个小诀窍,就是永远优先使用val来声明一个变量,而当val没有办法满足你的需求时再使用var。这样设计出来的程序会更加健壮,也更加符合高质量的编码规范。