第 1 章 开始启程,你的第一行Android代码
1.1 Android系统架构
Android大致可以分为4层架构:Linux内核层、系统运行库层、应用框架层和应用层。
1.1.1 Linux内核层
Android系统是基于Linux内核的,这一层为Android设备的各种硬件提供了底层的驱动,如显示驱动、音频驱动、照相机驱动、蓝牙驱动、Wi-Fi驱动、电源管理等。
1.1.2 系统运行库层
- 这一层通过一些C/C++库为Android系统提供了主要的特性支持。如SQLite库提供了数据库的支持,OpenGL|ES库提供了3D绘图的支持,Webkit库提供了浏览器内核的支持等。
- 在这一层还有Android运行时库,它主要提供了一些核心库,允许开发者使用Java语言来编写Android应用。另外,Android运行时库中还包含了Dalvik虚拟机(5.0系统之后改为ART运行环境),它使得每一个Android应用都能运行在独立的进程中,并且拥有一个自己的虚拟机实例。相较于Java虚拟机,Dalvik和ART都是专门为移动设备定制的,它针对手机内存、CPU性能有限等情况做了优化处理。
1.1.3 应用框架层
- 这一层主要提供了构建应用程序时可能用到的各种API,Android自带的一些核心应用就是使用这些API完成的,开发者可以使用这些API来构建自己的应用程序
1.1.4 应用层
- 所有安装在手机上的应用程序都是属于这一层的,比如系统自带的联系人、短信等程序,或者是你从GooglePlay上下载的小游戏,当然还包括你自己开发的程序。
1.2 Android已发布的版本
1.3 应用开发特色
1.3.1 四大组件
Activity
- Activity是所有Android应用程序的门面,凡是在应用中你看得到的东西,都是放在Activity中的。
Service
- 而Service就比较低调了,你无法看到它,但它会在后台默默地运行,即使用户退出了应用,Service仍然是可以继续运行的。
BroadcastReceiver
- BroadcastReceiver允许你的应用接收来自各处的广播消息,比如电话、短信等,当然,你的应用也可以向外发出广播消息。
ContentProvider
- ContentProvider则为应用程序之间共享数据提供了可能,比如你想要读取系统通讯录中的联系人,就需要通过ContentProvider来实现。
1.3.2 丰富的系统控件
1.3.3 SQLite数据库
1.3.4 强大的多媒体
1.4 手把手带你搭建开发环境
1.5 创建你的第一个Android项目
1.6 分析你的第一个Android程序
1.6.1 Project目录下的结构
- .gradle和.idea:这两个目录下放置的都是Android Studio自动生成的一些文件,无须关心,也不要去手动编辑。
- app:项目中的代码、资源等内容都是放置在这个目录下的,开发工作基本是在这个目录下进行的。
- build:这个目录主要包含了一些在编译时自动生成的文件,不需要过多关心。
- gradle:这个目录下包含了gradle wrapper的配置文件,使用gradlew rapper的方式不需要提前将gradle下载好,而是会自动根据本地的缓存情况决定是否需要联网下载gradle。
- .gitignore:这个文件是用来将指定的目录或文件排除在版本控制之外的。
- build.gradle:这是项目全局的gradle构建脚本,通常这个文件中的内容是不需要修改的。
- gradle.properties:这个文件是全局的gradle配置文件,在这里配置的属性将会影响到项目中所有的gradle编译脚本。
- gradlew和gradlew.bat:这两个文件是用来在命令行界面中执行gradle命令的,其中gradlew是在Linux或Mac系统中使用的,gradlew.bat是在Windows系统中使用的。
- shoppe.iml:iml文件是所有IntelliJ IDEA项目都会自动生成的一个文件(Android Studio是基于IntelliJ IDEA开发的),用于标识这是一个IntelliJ IDEA项目,我们不需要修改这个文件中的任何内容。
- local.properties:这个文件用于指定本机中的Android SDK路径,通常内容是自动生成的,我们并不需要修改。除非你本机中的Android SDK位置发生了变化,那么就将这个文件中的路径改成新的位置即可。
- settings.gradle:这个文件用于指定项目中所有引入的模块。通常情况下,模块的引入是自动完成的,需要我们手动修改这个文件的场景可能比较少。
1.6.2 app目录下的结构
- build:这个目录和外层的build目录类似,也包含了一些在编译时自动生成的文件,不过它里面的内容会更加复杂,不需要过多关心。
- libs:可存放项目中用到的第三方jar包,放在这个目录下的jar包会被自动添加到项目的构建路径里。 -androidTest:此处是用来编写Android Test测试用例的,可以对项目进行一些自动化测试。
- java:放置我们所有Java代码的地方(Kotlin代码也放在这里),展开该目录,你将看到系统帮我们自动生成了一个MainActivity文件。
- res:这个目录下的内容比较多。简单点说,就是你在项目中使用到的所有图片、布局、字符串等资源都要存放在这个目录下。当然这个目录下还有很多子目录,比如图片放在drawable目录下,布局放在layout目录下,字符串放在values目录下等等。
- AndroidManifest.xml:这是整个Android项目的配置文件,你在程序中定义的所有四大组件都需要在这个文件里注册,另外还可以在这个文件中给应用程序添加权限声明。
- test:此处是用来编写Unit Test测试用例的,是对项目进行自动化测试的另一种方式。
- .gitignore:这个文件用于将app模块内指定的目录或文件排除在版本控制之外,作用和外层的.gitignore文件类似。
- app.iml:IntelliJ IDEA项目自动生成的文件,不需要关心或修改这个文件中的内容。
- build.gradle:这是app模块的gradle构建脚本,这个文件中会指定很多项目构建相关的配置。
- proguard-rules.pro:这个文件用于指定项目代码的混淆规则,当代码开发完成后打包成安装包文件,如果不希望代码被别人破解,通常会将代码进行混淆,从而让破解者难以阅读。
1.6.3 详解项目中的资源
归纳一下,res目录中的内容就变得非常简单了。
- 所有以“drawable”开头的目录都是用来放图片的,所有以“mipmap”开头的目录都是用来放应用图标的,所有以“values”开头的目录都是用来放字符串、样式、颜色等配置的,所有以“layout”开头的目录都是用来放布局文件的。
- 之所以有这么多“mipmap”开头的目录,其实主要是为了让程序能够更好地兼容各种设备。
- drawable目录也是相同的道理,虽然Android Studio没有帮我们自动生成,但是我们应该自己创建drawable-hdpi、drawable-xhdpi、drawable-xxhdpi等目录。
- 在制作程序的时候,最好能够给同一张图片提供几个不同分辨率的版本,分别放在这些目录下,然后程序运行的时候,会自动根据当前运行设备分辨率的高低选择加载哪个目录下的图片。当然这只是理想情况,更多的时候美工只会提供给我们一份图片,这时你把所有图片都放在drawable-xxhdpi目录下就好了,因为这是最主流的设备分辨率目录。
1.6.4 详解build.gradle文件
1.6.4.1 根目录下的gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.5.10"
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
mavenCentral()
jcenter() // Warning: this repository is going to shut down soon
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
1.6.4.2 app目录下的gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.dream.shoppe"
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
1.7 前行必备:掌握日志工具的使用
# 第 2 章 探究新语言,快速入门Kotlin编程
2.1 Kotlin语言简介
- 编译型语言的特点是编译器会将我们编写的源代码一次性地编译成计算机可识别的二进制文件,然后计算机直接执行,像C和C++都属于编译型语言。
- 解释型语言则完全不一样,它有一个解释器,在程序运行时,解释器会一行行地读取我们编写的源代码,然后实时地将这些源代码解释成计算机可识别的二进制数据后再执行,因此解释型语言通常效率会差一些,像Python和JavaScript都属于解释型语言。
- 虽然Java代码确实是要先编译再运行的,但是Java代码编译之后生成的并不是计算机可识别的二进制文件,而是一种特殊的class文件,这种class文件只有Java虚拟机(Android中叫ART,一种移动优化版的虚拟机)才能识别,而这个Java虚拟机担当的其实就是解释器的角色,它会在程序运行时将编译后的class文件解释成计算机可识别的二进制数据后再执行,因此,准确来讲,Java属于解释型语言。
2.2 运行Kotlin
2.3 编程之本:变量和函数
2.3.1 变量
- val:(value的简写)用来声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋值,对应Java中的final变量。
- var:(variable的简写)用来声明一个可变的变量,这种变量在初始赋值之后仍然可以再被重新赋值,对应Java中的非final变量。
- Kotlin的类型推导机制:通过赋值时自动推断类型,但是Kotlin的类型推导机制并不总是可以正常工作的,比如说如果我们对一个变量延迟赋值的话,Kotlin就无法自动推导它的类型了。这时候就需要显式地声明变量类型才行:
//num将会在类型推断机制下成了int类型
val num = 10;
//显示声明num1为int类型
//此时kotlin不会自动推断类型
//因为显示声明了类型,若尝试赋值其它类型如字符串则会直接错误
val num1: Int = 10;
Kotlin中Int的首字母是大写的,而Java中int的首字母是小写的。不要小看这一个字母大小写的差距,这表示Kotlin完全抛弃了Java中的基本数据类型,全部使用了对象数据类型。在Java中int是关键字,而在Kotlin中Int变成了一个类,它拥有自己的方法和继承结构。
2.3.2 函数
/**
* 语法规则:
* "fun":(function的简写)是定义函数的关键字
* "methodName":函数名
* "paraX:Int":参数名:参数类型
* ":Int":返回值得类型,无返回值可省略不写。
* "{}":函数体
*/
fun methodName(paraX: Int, paraY: Int): Int {
return 0;
}
/**
* 当一个函数中只有一行代码时,Kotlin允许我们不必编写函数体,
* 可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可。
*/
fun largeNumber(paraX: Int, paraY: Int): Int = max(paraX, paraY)
/**
* max()函数返回的是Int类型
* 而类的自动推到机制使得largeNumber2返回值其实也成了Int类型
* 此时也就可以省略不声明返回值的类型
*/
fun largeNumber2(paraX: Int, paraY: Int) = max(paraX, paraY)
2.4 程序的逻辑控制
2.4.1 if条件语句
- Kotlin中的if语句相比于Java有一个额外的功能,它是可以有返回值的,返回值就是if语句每一个条件中最后一行代码的返回值。
fun largeNumber3(paraX: Int, paraY: Int) = if (paraX>paraY)paraX else paraY
2.4.2 when条件语句
- Kotlin中的when语句有点类似于Java中的switch语句,但它又远比switch语句强大得多。
**
* when语句允许传入一个任意类型的参数,然后可以在when的结构体中定义一系列的条件
* 当你的执行逻辑只有一行代码时,{ }可以省略
*/
fun getScore(name: String) = when (name) {
"张三" -> 0
"李四" -> 1
else -> 100
}
/**
* when语句还有一种不带参数的用法,
* 虽然这种用法可能不太常用,但有的时候却能发挥很强的扩展性。
* 这种用法是将判断的表达式完整地写在when的结构体当中。
* 注意,Kotlin中判断字符串或对象是否相等可以直接使用==关键字,
* 而不用像Java那样调用equals()方法。
*/
fun getScore2(name: String) = when {
name == "张三" -> 0
name == "李四" -> 1
else -> 100
}
/**
* when语句还允许进行类型匹配
* is关键字就是类型匹配的核心,它相当于Java中的instanceOf关键字
*/
fun checkNumber(num: Number) = when (num) {
is Int -> "Int"
is Double -> "Double"
else -> ""
}
2.4.3 循环语句
/**
* kotlin中存在区间的说法
* 该代码表示创建了一个0到10的区间,并且两端都是闭区间,
* 这意味着0到10这两个端点都是包含在区间中的,
* 用数学的方式表达出来就是[0, 10]。
*/
var range = 0..10
/**
* 一个左闭右开的区间,即不含有10
*/
range = 0 until 10
/**
* 一个左右皆闭合的降序区间
*/
var range2 = 10 downTo 1
/**
* 默认循环步数为1的for循环
*/
for (i in 10..15){
}
/**
* 自定义循环步数的for循环
*/
for (i in 10..15 step 3){
}
2.5 面向对象编程
不同于面向过程的语言(比如C语言),面向对象的语言是可以创建类的。
- 类就是对事物的一种封装,比如说人、汽车、房屋、书等任何事物,我们都可以将它封装一个类,类名通常是名词。而类中又可以拥有自己的字段和函数,字段表示该类所拥有的属性,比如说人可以有姓名和年龄,汽车可以有品牌和价格,这些就属于类中的字段,字段名通常也是名词。而函数则表示该类可以有哪些行为,比如说人可以吃饭和睡觉,汽车可以驾驶和保养等,函数名通常是动词。
2.5.1 类与对象
/**
*@author huadao
*@date 2021/7/22 18:13
*desc: kotlin也是使用class关键字声明一个类
*/
class Person {
var name = ""
fun eat(){
}
}
/**
* Kotlin去掉了new关键字
*/
val person = Person()
2.5.2 继承与构造函数
- Java与Kotlin在继承上的差异:在Java中,类默认可继承,而在Kotlin中任何一个非抽象类默认都是不可以被继承的,相当于Java中给类声明了final关键字。
- 之所以这么设计,其实和val关键字的原因是差不多的,因为类和变量一样,最好都是不可变的,而一个类允许被继承的话,它无法预知子类会如何实现,因此可能就会存在一些未知的风险。Effective Java这本书中明确提到,如果一个类不是专门为继承而设计的,那么就应该主动将它加上final声明,禁止它可以被继承。
/**
*@author huadao
*@date 2021/7/22 18:13
*desc: kotlin也是使用class关键字声明一个类
* open关键字标识允许被继承
*/
open class Person {
var name = ""
var age =0
lateinit var name1: String
var age1:Int =0
fun eat(){
}
}
/**
*@author huadao
*@date 2021/8/6 17:22
*desc:对于继承时写的括号涉及kotlin的主次函数问题,Kotlin中也有,但是Kotlin将构造函数分
* 成了两种:主构造函数和次构造函数。
* 主构造函数将会是你最常用的构造函数,每个类默认都会有一个不带参数的主构造函数,
* 当然你也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即可。
*
* Java继承特性中的一个规定,子类中的构造函数必须调用父类中的构造函数,这个规定在Kotlin中也要遵守。
* 而在Kotlin中子类的主构造函数调用父类中的哪个构造函数,在继承的时候通过括号来指定。
*
* Kotlin规定,当一个类既有主构造函数又有次构造函数时,
* 所有的次构造函数都必须调用主构造函数(包括间接调用)。
* 次构造函数是通过constructor关键字来定义的
*
* 类中只有次构造函数,没有主构造函数。这种情况真的十分少见,
* 但在Kotlin中是允许的。当一个类没有显式地定义主构造函数且定义了次构造函数时,
* 它就是没有主构造函数的,由于没有主构造函数,次构造函数只能直接调用父类的构造函数。
*/
class Student(name: String, age: Long, money: Int) : Person(money) {
//相当于java中在构造函数中做一些操作
init {
}
constructor(name: String) : this(name, 0, 0) {
}
constructor() : this("") {
}
}
2.5.3 接口
-
Java中继承使用的关键字是extends,实现接口使用的关键字是implements,而Kotlin中统一使用冒号,中间用逗号进行分隔。
-
为了让接口的功能更加灵活,Kotlin还增加了一个额外的功能:允许对接口中定义的函数进行默认实现。其实Java在JDK1.8之后也开始支持这个功能了,因此总体来说,Kotlin和Java在接口方面的功能仍然是一模一样的。
-
Java与Kotlin修饰符对比 | 修饰符 | Java |Kotlin| | --- | --- |--- | | public | 所有类可见 |所有类可见(默认)| | private | 当前类可见 |当前类可见| | protected | 当前类、子类、同一路径下的类可见 |当前类、子类可见| | default | 同一包路径下的类可见(默认,意如其字,只不过因为平时的开发中的类都会习惯性写上带上修饰符,造成了貌似不是这样的错觉) |无| | internal | 无 |同一模块中的类可见(可以简单理解为一个库一个组件内,不允许外使用的封闭式场景)|
2.5.4 数据类与单例类
在一个规范的系统架构中,数据类通常占据着非常重要的角色,它们用于将服务器端或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持。
数据类通常需要重写equals()、hashCode()、toString()这几个方法。其中,equals()方法用于判断两个数据类是否相等。hashCode()方法作为equals()的配套方法,也需要一起重写,否则会导致HashMap、HashSet等hash相关的系统类无法正常工作。toString()方法用于提供更清晰的输入日志,否则一个数据类默认打印出来的就是一行内存地址。
- Java数据类
/**
* @author huadao
* @date 2021/8/9 10:53
* desc:
*/
public class JavaBean {
public String name;
public long id;
@Override
public boolean equals(Object o) {
if (o instanceof JavaBean) {
JavaBean javaBean = (JavaBean) o;
return this.name.equals(javaBean.name) && this.id == javaBean.id;
}
return false;
}
@Override
public int hashCode() {
return (int) (name.hashCode() + id);
}
@Override
public String toString() {
return "JavaBean{" +
"name='" + name + ''' +
", id=" + id +
'}';
}
}
-Kotlin数据类
/**
*只需以data关键字声明,Kotlin会根据主构造函数中的参数帮你将equals()、
* hashCode()、toString()等固定且无实际逻辑意义的方法自动生成,
*从而大大减少了开发的工作量。
*@author huadao
*@date 2021/8/9 11:01
*desc:
*/
data class KotlinBean(val name:String,val id:Long) {
}
- Java单例类
/**
* @author huadao
* @date 2021/8/9 11:07
* desc:
*/
public class JavaSingleton {
private static JavaSingleton sInstance;
private JavaSingleton() {
}
public synchronized static JavaSingleton getInstance() {
if (sInstance == null) {
sInstance = new JavaSingleton();
}
return sInstace;
}
}
- Kotlin单例类
/**
*@author huadao
*@date 2021/8/9 11:18
*desc:在Kotlin中我们不需要私有化构造函数,
* 也不需要提供getInstance()这样的静态方法,
* 只需要把class关键字改成object关键字,一个单例类就创建完成了。
*/
object KotlinSingleton {
}
2.6 Lambda编程
2.6.1 集合的创建与遍历
- Java集合的创建
List<String> stringList =new ArrayList<>();
stringList.add("1");
stringList.add("2");
stringList.add("3");
- Kotlin listOf()函数创建的是一个不可变的集合。你也许不太能理解什么叫作不可变的集合,因为在Java中这个概念不太常见。不可变的集合指的就是该集合只能用于读取,我们无法对集合进行添加、修改或删除操作。 至于这么设计的理由,和val关键字、类默认不可继承的设计初衷是类似的,可见Kotlin在不可变性方面控制得极其严格。 使用mutableListOf()函数就可以创建一个可变集合。 而Map对应的无非就是setOf、mutableSetof
val stringList: MutableList<String> = ArrayList()
stringList.add("1")
stringList.add("2")
stringList.add("3")
val stringList1 = listOf<String>("1","2","3")
//Kotlin集合的遍历
for (value in stringList1){
}
//Map集合
val map = HashMap<String,String>()
//存储数据
map.put("apple","100")
map["apple"] = "100"
//读取一条数据
val 100 = map["apple"]
//更简洁的写法无非是
//这里的键值对组合看上去好像是使用to这个关键字来进行关联的,但其实to并不是关键字,而是一个infix函数.
val map = mapOf("name" to "zhangsan")
//map循环
//在for-in循环中,我们将Map的键值对变量一起声明到了一对括号里面,这样当进行循环遍历时,每次遍历的结果就会赋值给这两个键值对变量,最后将它们的值打印出来。
for((name,count) in map){
}
2.6.2 集合的函数式API
Lambda的定义,如果用最直白的语言来阐述的话,Lambda就是一小段可以作为参数传递的代码。从定义上看,这个功能就很厉害了,因为正常情况下,我们向某个函数传参时只能传入变量,而借助Lambda却允许传入一小段代码。这里两次使用了“一小段代码”这种描述,那么到底多少代码才算一小段代码呢?Kotlin对此并没有进行限制,但是通常不建议在Lambda表达式中编写太长的代码,否则可能会影响代码的可读性。
Lambda语法结构定义:{参数名1:参数类型,参数名2:参数类型->函数体}
//从一个list集合中遍历获取一个字符最长的名字
val maxLengthName = list.maxBy{nameList.length}
//简洁写法的演化
//首先,我们不需要专门定义一个lambda变量,而是可以直接将lambda表达式传入maxBy函数当中
val maxLengthName = list.maxBy({name:String -> nameList.length})
//首先,我们不需要专门定义一个lambda变量,而是可以直接将lambda表达式传入maxBy函数当中
val maxLengthName = list.maxBy{name:String -> nameList.length}
//由于Kotlin拥有出色的类型推导机制,Lambda表达式中的参数列表其实在大多数情况下不必声明参数类型
val maxLengthName = list.maxBy{name -> nameList.length}
//最后,当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替
val maxLengthName = list.maxBy{nameList.length}
- map函数 集合中的map函数是最常用的一种函数式API,它用于将集合中的每个元素都映射成一个另外的值,映射的规则在Lambda表达式中指定,最终生成一个新的集合。
- filter函数 filter函数是用来过滤集合中的数据的,它可以单独使用,也可以配合刚才的map函数一起使用。
- any函数 any函数用于判断集合中是否至少存在一个元素满足指定条件
- all函数 any函数用于判断集合中是否至少存在一个元素满足指定条件
2.6.3 Java函数式API的使用
如果我们在Kotlin代码中调用了一个Java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。Java单抽象方法接口指的是接口中只有一个待实现方法,如果接口中有多个待实现方法,则无法使用函数式API。 如果一个Java方法的参数列表中有且仅有一个Java单抽象方法接口参数,我们还可以将接口名进行省略。
Thread{
println("test")
}.star
2.7 空指针检查
2.7.1 可空类型系统
Kotlin利用编译时判空检查的机制几乎杜绝了空指针异常。
Kotlin默认所有的参数和变量都不可为空,也就是说,Kotlin将空指针异常的检查提前到了编译时期,如果我们的程序存在空指针异常的风险,那么在编译的时候会直接报错,修正之后才能成功运行,这样就可以保证程序在运行时期不会出现空指针异常了。
在类名的后面加上一个问号。比如,Int表示不可为空的整型,而Int?就表示可为空的整型;String表示不可为空的字符串,而String?就表示可为空的字符串。
2.7.2 判空辅助工具
- ?.操作符 当对象不为空时正常调用相应的方法,当对象为空时则什么都不做。
if(a!=null){
a.doSomething()
})
//使用操作符为
a.?doSomething(
- let let既不是操作符,也不是什么关键字,而是一个函数。这个函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到Lambda表达式中。
调用了obj对象的let函数,然后Lambda表达式中的代码就会立即执行,并且这个obj对象本身还会作为参数传递到Lambda表达式中。不过,为了防止变量重名,这里我将参数名改成了obj2,但实际上它们是同一个对象,这就是let函数的作用。
obj.let{obj2->
//编写具体的业务逻辑
}
- .?与let
fun doSomething(student:Student?){
student?.left{stu ->
stu.study()
}
}
//而当Lambda表达式的参数列表中只有一个参数时,可以不用声明参数名,
//直接使用it关键字来代替即可
fun doSomething(student:Student?){
student?.left{stu ->
it.study()
}
}
let函数是可以处理全局变量的判空问题的,而if判断语句则无法做到这一点。比如我们将doStudy()函数中的参数变成一个全局变量,使用let函数仍然可以正常工作,但使用if判断语句则会提示错误。
之所以这里会报错,是因为全局变量的值随时都有可能被其他线程所修改,即使做了判空处理,仍然无法保证if语句中的study变量没有空指针风险。从这一点上也能体现出let函数的优势。
2.8 Kotlin中的小魔术
2.8.1 字符串内嵌表达式
Kotlin允许我们在字符串里嵌入${}这种语法结构的表达式,并在运行时使用表达式执行的结果替代这一部分内容。
- hello,${obj.name}.nice to meet you! 另外,当表达式中仅有一个变量的时候,还可以将两边的大括号省略.
- hello,$name.nice to meet you!
2.8.2 函数的参数默认值
在定义函数的时候给任意参数设定一个默认值,这样当调用此函数时就不会强制要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。
可以通过键值对的方式来传参,从而不必像传统写法那样按照参数定义的顺序来传参。
第 3 章 先从看得到的入手,探究Activity
3.1 Activity是什么
它是一种可以包含用户界面的组件,主要用于和用户进行交互。
3.2 Activity的基本用法
3.2.1 手动创建Activity
3.2.2 创建和加载布局
3.2.3 在AndroidManifest文件中注册
3.2.4 在Activity中使用Toast
3.2.5 在Activity中使用Menu
3.2.6 销毁一个Activity
3.3 使用Intent在Activity之间穿梭
3.3.1 使用显式Intent
Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据,Intent一般可用于启动Activity、启动Service以及发送广播等场景。 Intent大致可以分为两种:显式Intent和隐式Intent。
3.3.2 使用隐式Intent
每个Intent中只能指定一个action,但能指定多个category。
3.3.3 更多隐式Intent的用法
- android:scheme。用于指定数据的协议部分,如上例中的https部分。
- android:host。用于指定数据的主机名部分,如上例中的www.baidu.com部分。
- android:port。用于指定数据的端口部分,如上例中的8080部分。
- android:path。用于指定主机名和端口之后的部分,如上例中的xxx部分。
- android:mimeType。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
3.3.4 向下一个Activity传递数据
3.3.5 返回数据给上一个Activity
3.4 Activity的生命周期
Android是使用任务(task)来管理Activity的,一个任务就是一组存放在栈里的Activity的集合,这个栈也被称作返回栈(back stack)。栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。而每当我们按下Back键或调用finish()方法去销毁一个Activity时,处于栈顶的Activity就会出栈,前一个入栈的Activity就会重新处于栈顶的位置。系统总是会显示处于栈顶的Activity给用户。
3.4.2 Activity状态
每个Activity在其生命周期中最多可能会有4种状态。
- 运行状态 当一个Activity位于返回栈的栈顶时,Activity就处于运行状态。系统最不愿意回收的就是处于运行状态的Activity,因为这会带来非常差的用户体验。
- 暂停状态 当一个Activity不再处于栈顶位置,但仍然可见时,Activity就进入了暂停状态。你可能会觉得,既然Activity已经不在栈顶了,怎么会可见呢?这是因为并不是每一个Activity都会占满整个屏幕,比如对话框形式的Activity只会占用屏幕中间的部分区域。处于暂停状态的Activity仍然是完全存活着的,系统也不愿意回收这种Activity(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种Activity。
- 停止状态 当一个Activity不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种Activity保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的Activity有可能会被系统回收。
- 销毁状态 一个Activity从返回栈中移除后就变成了销毁状态。系统最倾向于回收处于这种状态的Activity,以保证手机的内存充足。
3.4.3 Activity的生命周期
- onCreate()。 它会在Activity第一次被创建的时候调用。你应该在这个方法中完成Activity的初始化操作,比如加载布局、绑定事件等。
- onStart()。 这个方法在Activity由不可见变为可见的时候调用。
- onResume()。 这个方法在Activity准备好和用户进行交互的时候调用。此时的Activity一定位于返回栈的栈顶,并且处于运行状态。
- onPause()。 这个方法在系统准备去启动或者恢复另一个Activity的时候调用。我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶Activity的使用。
- onStop()。 这个方法在Activity完全不可见的时候调用。它和onPause()方法的主要区别在于,如果启动的新Activity是一个对话框式的Activity,那么onPause()方法会得到执行,而onStop()方法并不会执行。
- onDestroy()。 这个方法在Activity被销毁之前调用,之后Activity的状态将变为销毁状态。
- onRestart()。 这个方法在Activity由停止状态变为运行状态之前调用,也就是Activity被重新启动了。
- 完整生存期。 Activity在onCreate()方法和onDestroy()方法之间所经历的就是完整生存期。一般情况下,一个Activity会在onCreate()方法中完成各种初始化操作,而在onDestroy()方法中完成释放内存的操作。
- 可见生存期。 Activity在onStart()方法和onStop()方法之间所经历的就是可见生存期。在可见生存期内,Activity对于用户总是可见的,即便有可能无法和用户进行交互。我们可以通过这两个方法合理地管理那些对用户可见的资源。比如在onStart()方法中对资源进行加载,而在onStop()方法中对资源进行释放,从而保证处于停止状态的Activity不会占用过多内存。
- 前台生存期。 Activity在onResume()方法和onPause()方法之间所经历的就是前台生存期。在前台生存期内,Activity总是处于运行状态,此时的Activity是可以和用户进行交互的,我们平时看到和接触最多的就是这个状态下的Activity。
3.4.4 体验Activity的生命周期
3.4.5 Activity被回收了怎么办
onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的内容。
另外,当手机的屏幕发生旋转的时候,Activity也会经历一个重新创建的过程,因而在这种情况下,Activity中的数据也会丢失。虽然这个问题同样可以通过onSaveInstanceState()方法来解决,但是一般不太建议这么做,因为对于横竖屏旋转的情况,现在有更加优雅的解决方案
3.5 Activity的启动模式
3.5.1 standard
standard是Activity默认的启动模式,在不进行显式指定的情况下,所有Activity都会自动使用这种启动模式。
在standard模式下,每当启动一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的Activity,系统不会在乎这个Activity是否已经在返回栈中存在,每次启动都会创建一个该Activity的新实例。
3.5.2 singleTop
3.5.3 singleTask
3.5.4 singleInstance
3.6 Activity的最佳实践
3.6.1 知晓当前是在哪一个Activity
BaseActivity与log的结合
3.6.2 随时随地退出程序
Acitivity集合管理与进程kill的结合
3.6.3 启动Activity的最佳写法
companion object构造一个静态启动activity的函数
3.7 Kotlin课堂:标准函数和静态方法
3.7.1 标准函数with、run和apply
Kotlin的标准函数指的是Standard.kt文件中定义的函数,任何Kotlin代码都可以自由地调用所有的标准函数。
- with函数 with函数接收两个参数:第一个参数可以是一个任意类型的对象,第二个参数是一个Lambda表达式。with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回。
val result = with(obj){
//这里是obj的上下文
“value”//with函数的返回值
}
//它可以在连续调用同一个对象的多个方法时让代码变得更加精简。
val result = with(StringBuilder()){
append("Start eatingfruits.\n")
for(fruit in list){
append("apple").append("orange")
}
toString()
}
- run函数 run函数的用法和使用场景其实和with函数是非常类似的,只是稍微做了一些语法改动而已。首先run函数通常不会直接调用,而是要在某个对象的基础上调用;其次run函数只接收一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文。其他方面和with函数是一样的,包括也会使用Lambda表达式中的最后一行代码作为返回值返回。
val result = obj.run{
//这里是obj的上下文
“value”//with函数的返回值
}
//使用run
val result = StringBuilder().run{
append("Start eatingfruits.\n")
for(fruit in list){
append("apple").append("orange")
}
toString()
}
- apply函数 apply函数和run函数也是极其类似的,都要在某个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。
val result = obj.apply{
//这里是obj的上下文
}
return result == obj
//使用apply
val result = StringBuilder().apply{
append("Start eatingfruits.\n")
for(fruit in list){
append("apple").append("orange")
}
}
result.toString()
3.7.2 定义静态方法
静态方法在某些编程语言里面又叫作类方法,指的就是那种不需要创建实例就能调用的方法,所有主流的编程语言都会支持静态方法这个特性。
但是和绝大多数主流编程语言不同的是,Kotlin却极度弱化了静态方法这个概念,想要在Kotlin中定义一个静态方法反倒不是一件容易的事。
因为Kotlin提供了比静态方法更好用的语法特性,那就是单例类。
使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,而如果我们只是希望让类中的某一个方法变成静态方法的调用方式的话,这个时候就可以使用companion object了。
companion object这个关键字实际上会在当前类的内部创建一个伴生类,而修饰的方法就是定义在这个伴生类里面的实例方法。只是Kotlin会保证当前类始终只会存在一个伴生类对象,因此调用该静态方法实际上就是调用了当前类中伴生对象的方法。
由此可以看出,Kotlin确实没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似于静态方法调用的写法,这些语法特性基本可以满足我们平时的开发需求了。
然而如果你确确实实需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层方法。
- 注解 单例类和companion object都只是在语法的形式上模仿了静态方法的调用方式,实际上它们都不是真正的静态方法。因此如果你在Java代码中以静态方法的形式去调用的话,你会发现这些方法并不存在。而如果我们给单例类或companion object中的方法加上@JvmStatic注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法。
注意,@JvmStatic注解只能加在单例类或companion object中的方法上,如果你尝试加在一个普通方法上,会直接提示语法错误。
- 顶层 顶层方法指的是那些没有定义在任何类中的方法,Kotlin编译器会将所有的顶层方法全部编译成静态方法,因此只要你定义了一个顶层方法,那么它就一定是静态方法。
但如果是在Java代码中调用,会找不到所定义的顶层方法,因为Java中没有顶层方法这个概念,所有的方法必须定义在类中。
顶层方法会存根据创建的Kotlin文件名xxx.kt自动创建一个叫作xxxKt的Java类,顶层方法就是以静态方法的形式定义在xxxKt类里面的,因此在Java里可通过xxKt类进行方法调用。
第 4 章 软件也要拼脸蛋,UI开发的点点滴滴
4.1 该如何编写程序界面
4.2 常用控件的使用方法
4.2.1 TextView
4.2.2 Button
4.2.3 EditText
4.2.4 ImageView
4.2.5 ProgressBar
4.2.6 AlertDialog
4.3 详解3种基本布局
4.3.1 LinearLayout
4.3.2 RelativeLayout
4.3.3 FrameLayout
4.4 系统控件不够用?创建自定义控件
4.4.1 引入布局
引入布局的技巧确实解决了重复编写布局代码的问题
4.4.2 创建自定义控件
但是如果布局中有一些控件要求能够响应事件,我们还是需要在每个Activity中为这些控件单独编写一次事件注册的代码。比如标题栏中的返回按钮,其实不管是在哪一个Activity中,这个按钮的功能都是相同的,即销毁当前Activity。而如果在每一个Activity中都需要重新注册一遍返回按钮的点击事件,无疑会增加很多重复代码,这种情况最好是使用自定义控件的方式来解决。
4.5 最常用和最难用的控件:ListView
4.5.1 ListView的简单用法
4.5.2 定制ListView的界面
4.5.3 提升ListView的运行效率
4.5.4 ListView的点击事件
4.6 更强大的滚动控件:RecyclerView
4.6.1 RecyclerView的基本用法
4.6.2 实现横向滚动和瀑布流布局
4.6.3 RecyclerView的点击事件
4.7 编写界面的最佳实践
4.7.1 制作9-Patch图片
4.7.2 编写精美的聊天界面
4.8 Kotlin课堂:延迟初始化和密封类
4.8.1 对变量延迟初始化
延迟初始化使用的是lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为null了。
对一个全局变量使用了lateinit关键字时,一定要确保它在被任何地方调用之前已经完成了初始化工作,否则Kotlin将无法保证程序的安全性。
可以通过代码来判断一个全局变量是否已经完成了初始化,这样在某些时候能够有效地避免重复对某一个变量进行初始化操作。
::obj.isLazyInit()
4.8.2 使用密封类优化代码
密封类的关键字是sealed class
interface Result
class Success : Result
//由于密封类是一个可继承的类,因此在继承它的时候需要在后面加上一对括号
sealed class Result
class Success : Result()
当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应的条件全部处理。这样就可以保证,即使没有编写else条件,也不可能会出现漏写条件分支的情况。而如果我们现在新增一个Unknown类,并也让它继承自Result,此时getResultMsg()方法就一定会报错,必须增加一个Unknown的条件分支才能让代码编译通过。
密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。
第 5 章 手机平板要兼顾,探究Fragment
5.1 Fragment是什么
Fragment是一种可以嵌入在Activity当中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间,因而在平板上应用得非常广泛。
5.2 Fragment的使用方式
5.2.1 Fragment的简单用法
5.2.2 动态添加Fragment
动态添加Fragment主要分为5步。 (1) 创建待添加Fragment的实例。(2) 获取FragmentManager,在Activity中可以直接调用getSupportFragmentManager()方法获取。(3) 开启一个事务,通过调用beginTransaction()方法开启。(4) 向容器内添加或替换Fragment,一般使用replace()方法实现,需要传入容器的id和待添加的Fragment实例。(5) 提交事务,调用commit()方法来完成。
5.2.3 在Fragment中实现返回栈
addToBackStack()
5.2.4 Fragment和Activity之间的交互
5.3 Fragment的生命周期
5.3.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解除关联时调用。
5.3.2 体验Fragment的生命周期
5.4 动态加载布局的技巧
5.4.1 使用限定符
5.4.2 使用最小宽度限定符
最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一个布局。
5.5 Fragment的最佳实践:一个简易版的新闻应用
5.6 Kotlin课堂:扩展函数和运算符重载
5.6.1 大有用途的扩展函数
扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。
相比于定义一个普通的函数,定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中了。
扩展函数在很多情况下可以让API变得更加简洁、丰富,更加面向对象。
5.6.2 有趣的运算符重载
运算符重载使用的是operator关键字,只要在指定函数的前面加上operator关键字,就可以实现运算符重载的功能了。
第 6 章 全局大喇叭,详解广播机制
6.1 广播机制简介 Android中的广播主要可以分为两种类型:标准广播和有序广播。 标准广播(normal broadcasts)是一种完全异步执行的广播,在广播发出之后,所有的BroadcastReceiver几乎会在同一时刻收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。 有序广播(ordered broadcasts)则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个BroadcastReceiver能够收到这条广播消息,当这个BroadcastReceiver中的逻辑执行完毕后,广播才会继续传递。所以此时的BroadcastReceiver是有先后顺序的,优先级高的BroadcastReceiver就可以先收到广播消息,并且前面的BroadcastReceiver还可以截断正在传递的广播,这样后面的BroadcastReceiver就无法收到广播消息了。
6.2 接收系统广播 注册BroadcastReceiver的方式一般有两种:在代码中注册和在AndroidManifest.xml中注册。其中前者也被称为动态注册,后者也被称为静态注册。 6.2.1 动态注册监听时间变化
动态注册的BroadcastReceiver可以自由地控制注册与注销,在灵活性方面有很大的优势。但是它存在着一个缺点,即必须在程序启动之后才能接收广播,因为注册的逻辑是写在onCreate()方法中的。 6.2.2 静态注册实现开机启动 其实从理论上来说,动态注册能监听到的系统广播,静态注册也应该能监听到,在过去的Android系统中确实是这样的。但是由于大量恶意的应用程序利用这个机制在程序未启动的情况下监听系统广播,从而使任何应用都可以频繁地从后台被唤醒,严重影响了用户手机的电量和性能,因此Android系统几乎每个版本都在削减静态注册BroadcastReceiver的功能。 在Android 8.0系统之后,所有隐式广播都不允许使用静态注册的方式来接收了。隐式广播指的是那些没有具体指定发送给哪个应用程序的广播,大多数系统广播属于隐式广播,但是少数特殊的系统广播目前仍然允许使用静态注册的方式来接收。
6.3 发送自定义广播 6.3.1 发送标准广播 sendBroadcast() 在Android 8.0系统之后,静态注册的BroadcastReceiver是无法接收隐式广播的,而默认情况下我们发出的自定义广播恰恰都是隐式广播。因此这里一定要调用setPackage()方法,指定这条广播是发送给哪个应用程序的,从而让它变成一条显式广播,否则静态注册的BroadcastReceiver将无法接收到这条广播。 6.3.2 发送有序广播 sendOrderedBroadcast() 通过android:priority属性给BroadcastReceiver设置了优先级,优先级比较高的BroadcastReceiver就可以先收到广播。 6.4 广播的最佳实践:实现强制下线功能 6.5 Kotlin课堂:高阶函数详解 6.5.1 定义高阶函数 如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。 编程语言中有整型、布尔型等字段类型,而Kotlin又增加了一个函数类型的概念。如果我们将这种函数类型添加到一个函数的参数声明或者返回值声明当中,那么这就是一个高阶函数了。
//(String,Int)部分,声明该函数接收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了
//Unit部分,声明该函数的返回值是什么类型,如果没有返回值就使用Unit,它大致相当于Java中的void。
(String,Int) -> Unit
fun exampleMethod(func:(String,Int)->Int){
doSomethingMethod()
}
高阶函数的用途实在是太广泛了,可简单概括为:高阶函数允许让函数类型的参数来决定函数的执行逻辑。即使是同一个高阶函数,只要传入不同的函数类型参数,那么它的执行逻辑和最终的返回结果就可能是完全不同的。
6.5.2 内联函数的作用
shiLambda表达式在使用时,它会在底层被转换成了匿名类的实现方式。这就表明,我们每调用一次Lambda表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。
Kotlin的内联函数,它可以将使用Lambda表达式带来的运行时开销完全消除。内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字的声明即可。
内联函数的工作原理就是Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。
6.5.3 noinline与crossinline
只想内联其中的一个Lambda表达式是,可以用noinline进行声明
Kotlin之所以提供一个noinline关键字来排除内联功能,这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。
另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回 。 将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况。如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。
在这种情况下若还是使用内联函数则借助crossinline关键字就可以很好地解决这个问题。
6.6 Git时间:初识版本控制工具
6.6.1 安装Git
6.6.2 创建代码仓库
6.6.3 提交本地代码
第 7 章 数据存储全方案,详解持久化技术
7.1 持久化技术简介
数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或计算机关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的。持久化技术提供了一种机制,可以让数据在瞬时状态和持久状态之间进行转换。
7.2 文件存储
文件存储是Android中最基本的数据存储方式,它不对存储的内容进行任何格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合存储一些简单的文本数据或二进制数据。如果你想使用文件存储的方式来保存一些较为复杂的结构化数据,就需要定义一套自己的格式规范,方便之后将数据从文件中重新解析出来。
7.2.1 将数据存储到文件中
7.2.2 从文件中读取数据
7.3 SharedPreferences存储
SharedPreferences是使用键值对的方式来存储数据的。
7.3.1 将数据存储到SharedPreferences中
-
Context类中的getSharedPreferences()方法
-
Activity类中的getPreferences()方法
得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要可以分为3步实现。
-
(1) 调用SharedPreferences对象的edit()方法获取一个SharedPreferences.Editor对象。
-
(2) 向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法,添加一个字符串则使用putString()方法,以此类推。
-
(3) 调用apply()方法将添加的数据提交,从而完成数据存储操作。
7.3.2 从SharedPreferences中读取数据
7.3.3 实现记住密码功能
7.4 SQLite数据库存储
SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务。
7.4.1 创建数据库
SQLiteOpenHelper
7.4.2 升级数据库
7.4.3 添加数据
7.4.4 更新数据
7.4.5 删除数据
7.4.6 查询数据
7.4.7 使用SQL操作数据库
7.5 SQLite数据库的最佳实践
7.5.1 使用事务
SQLite数据库是支持事务的,事务的特性可以保证让一系列的操作要么全部完成,要么一个都不会完成。
7.5.2 升级数据库的最佳写法
7.6 Kotlin课堂:高阶函数的应用
高阶函数非常适用于简化各种API的调用,一些API的原有用法在使用高阶函数简化之后,不管是在易用性还是可读性方面,都可能会有很大的提升。
7.6.1 简化SharedPreferences的用法
7.6.2 简化ContentValues的用法
第 8 章 跨程序共享数据,探究ContentProvider
8.1 ContentProvider简介
ContentProvider主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。目前,使用ContentProvider是Android实现跨程序共享数据的标准方式。
不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,ContentProvider可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。
8.2 运行时权限
8.2.1 Android权限机制详解
8.2.2 在程序运行时申请权限
8.3 访问其他程序中的数据
8.3.1 ContentResolver的基本用法
不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。内容URI给ContentProvider中的数据建立了唯一标识符,它主要由两部分组成:authority和path。authority是用于对不同的应用程序做区分的,一般为了避免冲突,会采用应用包名的方式进行命名。
8.3.2 读取系统联系人
8.4 创建自己的ContentProvider
8.4.1 创建ContentProvider的步骤
- (1) onCreate()。初始化ContentProvider的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true表示ContentProvider初始化成功,返回false则表示失败。
- (2) query()。从ContentProvider中查询数据。uri参数用于确定查询哪张表,projection参数用于确定查询哪些列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
- (3) insert()。向ContentProvider中添加一条数据。uri参数用于确定要添加到的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新记录的URI。
- (4) update()。更新ContentProvider中已有的数据。uri参数用于确定更新哪一张表中的数据,新数据保存在values参数中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。
- (5) delete()。从ContentProvider中删除数据。uri参数用于确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
- (6) getType()。根据传入的内容URI返回相应的MIME类型。 *表示匹配任意长度的任意字符。#表示匹配任意长度的数字。
8.4.2 实现跨程序数据共享
8.5 Kotlin课堂:泛型和委托
8.5.1 泛型的基本用法
在一般的编程模式下,我们需要给任何一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。
泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是。当然括号内的T并不是固定要求的,事实上你使用任何英文字母或单词都可以,但是通常情况下,T是一种约定俗成的泛型写法。
//泛型类、泛型方法、泛型参数
class MyClass<T>{
fun <T>method(param:T):T{
}
}
在默认情况下,所有的泛型都是可以指定成可空类型的,这是因为在不手动指定上界的时候,泛型的上界默认是Any?。而如果想要让泛型的类型不可为空,只需要将泛型的上界手动指定成Any就可以了。
8.5.2 类委托和委托属性
类委托
委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。
类委托,它的核心思想在于将一个类的具体实现委托给另一个类去完成。
Kotlin中委托使用的关键字是by,我们只需要在接口声明的后面使用by关键字,再接上受委托的辅助对象,就可以免去之前所写的一大堆模板式的代码了。
委托属性
委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。
8.5.3 实现一个自己的lazy函数
第 9 章 丰富你的程序,运用手机多媒体
9.1 将程序运行到手机上
9.2 使用通知
9.2.1 创建通知渠道
9.2.2 通知的基本用法
9.2.3 通知的进阶技巧
9.3 调用摄像头和相册
9.3.1 调用摄像头拍照
9.3.2 从相册中选择图片
9.4 播放多媒体文件
9.4.1 播放音频
9.4.2 播放视频
9.5 Kotlin课堂:使用infix函数构建更可读的语法
to并不是Kotlin语言中的一个关键字,之所以我们能够使用A to B这样的语法结构,是因为Kotlin提供了一种高级语法糖特性:infix函数。 infix函数允许我们将函数调用时的小数点、括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编写程序,让代码看起来更加具有可读性。
9.6 Git时间:版本控制工具进阶
第 10 章 后台默默的劳动者,探究Service
10.1 Service是什么
Service是Android中实现程序后台运行的解决方案,它非常适合执行那些不需要和用户交互而且还要求长期运行的任务。Service的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,Service仍然能够保持正常运行。
Service并不是运行在一个独立的进程当中的,而是依赖于创建Service时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的Service也会停止运行。
Service并不会自动开启线程,所有的代码都是默认运行在主线程当中的。也就是说,我们需要在Service的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞的情况。
10.2 Android多线程编程
10.2.1 线程的基本用法
10.2.2 在子线程中更新UI
Android异步消息处理的基本用法,使用这种机制就可以出色地解决在子线程中更新UI的问题。
10.2.3 解析异步消息处理机制
Android中的异步消息处理主要由4个部分组成:Message、Handler、MessageQueue和Looper。
-
MessageMessage是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间传递数据。上一小节中我们使用到了Message的what字段,除此之外还可以使用arg1和arg2字段来携带一些整型数据,使用obj字段携带一个Object对象。
-
HandlerHandler顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用Handler的sendMessage()方法、post()方法等,而发出的消息经过一系列地辗转处理后,最终会传递到Handler的handleMessage()方法中。
-
MessageQueueMessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。
-
LooperLooper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入一个无限循环当中,然后每当发现MessageQueue中存在一条消息时,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中只会有一个Looper对象。
10.2.4 使用AsyncTask
- Params。
在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。
- Progress。
在后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
-
onResult。 当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
-
onPreExecute() 这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
-
doInBackground(Params...)这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成,就可以通过return语句将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是Unit,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress (Progress...)方法来完成。
-
onProgressUpdate(Progress...) 当在后台任务中调用了publishProgress(Progress...)方法后,onProgressUpdate (Progress...)方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。
-
onPostExecute(Result)当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据进行一些UI操作,比如说提醒任务执行的结果,以及关闭进度条对话框等。
10.3 Service的基本用法
10.3.1 定义一个Service
10.3.2 启动和停止Service
10.3.3 Activity和Service进行通信
10.4 Service的生命周期
10.5 Service的更多技巧
10.5.1 使用前台Service
从Android 8.0系统开始,只有当应用保持在前台可见状态的情况下,Service才能 保证稳定运行,一旦应用进入后台之后,Service随时都有可能被系统回收。而如果你希望Service能够一直保持运行状态,就可以考虑使用前台Service。前台Service和普通Service最大的区别就在于,它一直会有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。
从Android 9.0系统开始,使用前台Service必须在AndroidManifest.xml文件中进行权限声明才行
10.5.2 使用IntentService
Service中的代码都是默认运行在主线程当中的,如果直接在Service里处理一些耗时的逻辑,就很容易出现ANR(Application NotResponding)的情况。 ##10.6 Kotlin课堂:泛型的高级 特性
10.6.1 对泛型进行实化
泛型实化这个功能对于绝大多数Java程序员来讲是非常陌生的,因为Java中完全没有这个概念。而如果我们想要深刻地理解泛型实化,就要先解释一下Java的泛型擦除机制才行。
Java的泛型功能是通过类型擦除机制来实现的。什么意思呢?就是说泛型对于类型的约束只在编译时期存在,运行的时候仍然会按照JDK 1.5之前的机制来运行,JVM是识别不出来我们在代码中指定的泛型类型的。例如,假设我们创建了一个List集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行时期JVM并不能知道它本来只打算包含哪种类型的元素,只能识别出来它是个List。
所有基于JVM的语言,它们的泛型功能都是通过类型擦除机制来实现的,其中当然也包括了Kotlin。这种机制使得我们不可能使用a is T或者T::class.java这样的语法,因为T的实际类型在运行的时候已经被擦除了。
然而不同的是,Kotlin提供了一个内联函数的概念,我们在第6章的Kotlin课堂中已经学过了这个知识点。内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样的话也就不存在什么泛型擦除的问题了,因为代码在编译之后会直接使用实际的类型来替代内联函数中的泛型声明。
这就意味着,Kotlin中是可以将内联函数中的泛型进行实化的。
那么具体该怎么写才能将泛型实化呢?首先,该函数必须是内联函数才行,也就是要用inline关键字来修饰该函数。其次,在声明泛型的地方必须加上reifed关键字来表示该泛型要进行实化。
inline fun<reified T> doSomething(){
}
10.6.2 泛型实化的应用 泛型实化功能允许我们在泛型函数当中获得泛型的实际类型,这也就使得类似于a is T、T::class.java这样的语法成为了可能。
inline launch<refied T>(Context context){
context.startActivity(new Intent(cotext,::T.class.java))
}
//启动TestActivity
startAxtivity<TestActivity>(context)
Kotlin将能够识别出指定泛型的实际类型,并启动相应的Activity。
10.6.3 泛型的协变
一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为in位置,而它的返回值是输出数据的地方,因此可以称它为out位置
假如定义了一个MyClass的泛型类,其中A是B的子类型,同时MyClass又是MyClass的子类型,那么我们就可以称MyClass在T这个泛型上是协变的。
10.6.4 泛型的逆变
假如定义了一个MyClass的泛型类,其中A是B的子类型,同时MyClass又是MyClass的子类型,那么我们就可以称MyClass在T这个泛型上是逆变。
Kotlin在提供协变和逆变功能时,就已经把各种潜在的类型转换安全隐患全部考虑进去了。只要我们严格按照其语法规则,让泛型在协变时只出现在out位置上,逆变时只出现在in位置上,就不会存在类型转换异常的情况。虽然@UnsafeVariance注解可以打破这一语法规则,但同时也会带来额外的风险,所以你在使用@UnsafeVariance注解时,必须很清楚自己在干什么才行。
第 11 章 看看精彩的世界,使用网络技术
11.1 WebView的用法
11.2 使用HTTP访问网络
11.2.1 使用HttpURLConnection
在过去,Android上发送HTTP请求一般有两种方式:HttpURLConnection和HttpClient。不过由于HttpClient存在API数量过多、扩展困难等缺点,Android团队越来越不建议我们使用这种方式。终于在Android 6.0系统中,HttpClient的功能被完全移除了,标志着此功能被正式弃用,
11.2.2 使用OkHttp
11.3 解析XML格式数据
11.3.1 Pull解析方式
11.3.2 SAX解析方式
11.4 解析JSON格式数据
11.4.1 使用JSONObject
11.4.2 使用GSON
11.5 网络请求回调的实现方式
11.6 最好用的网络库:Retrofit
11.7 Kotlin课堂:使用协程编写高效的并发程序
它其实和线程是有点类似的,可以简单地将它理解成一种轻量级的线程。要知道,我们之前所学习的线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换。而使用协程却可以仅在编程语言的层面就能实现不同协程之间的切换,从而大大提升了并发编程的运行效率。
协程允许我们在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关。这种特性使得高并发程序的运行效率得到了极大的提升。
11.7.1 协程的基本用法
- Global.launch函数
GlobalScope.launch函数可以创建一个协程的作用域,这样传递给launch函数的代码块(Lambda表达式)就是在协程中运行的了。
-
runBlocking函 runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题。
-
launch函数 使用launch函数就可以创建多个协程
launch函数和我们刚才所使用的GlobalScope.launch函数不同。首先它必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。相比而言,GlobalScope.launch函数创建的永远是顶层协程,这一点和线程比较像,因为线程也没有层级这一说,永远都是顶层的。
- suspend关键字
使用suspend关键字可以将任意函数声明成挂起函数,而挂起函数之间都是可以互相调用的。
suspend关键字只能将一个函数声明成挂起函数,是无法给它提供协程作用域的。比如你现在尝试在printDot()函数中调用launch函数,一定是无法调用成功的,因为launch函数要求必须在协程作用域当中才能调用。
- coroutineScope函数 coroutineScope函数也是一个挂起函数,因此可以在任何其他挂起函数中调用。它的特点是会继承外部的协程的作用域并创建一个子协程,借助这个特性,我们就可以给任意挂起函数提供协程作用域了。
coroutineScope函数和runBlocking函数还有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起。
coroutineScope函数确实是将外部协程挂起了,只有当它作用域内的所有代码和子协程都执行完毕之后,coroutineScope函数之后的代码才能得到运行。
11.7.2 更多的作用域构建器
那么协程要怎样取消呢?不管是GlobalScope.launch函数还是launch函数,它们都会返回一个Job对象,只需要调用Job对象的cancel()方法就可以取消协程了。
但是如果我们每次创建的都是顶层协程,那么当Activity关闭时,就需要逐个调用所有已创建协程的cancel()方法,试想一下,这样的代码是不是根本无法维护?因此,GlobalScope.launch这种协程作用域构建器,在实际项目中也是不太常用的。
那么有没有什么办法能够创建一个协程并获取它的执行结果呢?当然有,使用async函数就可以实现。
withContext()函数。withContext()函数是一个挂起函数,大体可以将它理解成async函数的一种简化版写法。
调用withContext()函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为withContext()函数的返回值返回,因此基本上相当于val result= async{ 5 + 5 }.await()的写法。唯一不同的是,withContext()函数强制要求我们指定一个线程参数,关于这个参数我准备好好讲一讲。
线程参数主要有以下3种值可选:Dispatchers.Default、Dispatchers.IO和Dispatchers.Main。
Dispatchers.Default表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。
Dispatchers.IO表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用Dispatchers.IO。
Dispatchers.Main则表示不会开启子线程,而是在Android主线程中执行代码,但是这个值只能在Android项目中使用,纯Kotlin程序使用这种类型的线程参数会出现错误。
11.7.3 使用协程简化回调的写法
suspendCoroutine函数
suspendCoroutine函数必须在协程作用域或挂起函数中才能调用,它接收一个Lambda表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行Lambda表达式中的代码。Lambda表达式的参数列表上会传入一个Continuation参数,调用它的resume()方法或resumeWithException()可以让协程恢复执行。
第 12 章 最佳的UI体验,Material Design实战
第 13 章 高级程序开发组件,探究Jetpack
13.1 Jetpack简介
Jetpack是一个开发组件工具集,它的主要目的是帮助我们编写出更加简洁的代码,并简化我们的开发过程。Jetpack中的组件有一个特点,它们大部分不依赖于任何Android系统版本,这意味着这些组件通常是定义在AndroidX库当中的,并且拥有非常好的向下兼容性。