低级安卓工程师之路(二)
分析第一个Android 程序
app目录下的内容分析
1.build 包含了编译时自动生成的文件
2.libs 如果使用到了第三方jar包,就需要把这些jar包放在libs目录下,放在这个目录下的jar包会自动添加到项目的构建路径里。
3.androidTest 编写Android Test 测试用例的,可对项目进行一些自动化测试
4.Java java目录是放置java代码的地方(Kotlin代码也放在这里)
5.res 存放图片,布局,字符串等资源。
6.AndroidManifest.xml 整个Android 项目的配置文件,程序中定义的四大组件需要在这个文件里注册,可以在这个文件中给应用程序添加权限说明。
7.test 编写Unit Test 测试用例的,是对项目进行自动化测试的另一种方式。
8.gitignore 用于将app模板内指定的文件或目录排除在版本控制之外
9.app.iml IDEA项目自动生成的文件
10.build.gradle app模块的gradle构建脚本,这个文件中会指定很多项目构建相关的配置。
11.proguard-rules.pro 用于指定项目代码的混淆规则,当代码开发完成后打包成安装文件,如果不希望被别人破解,通常会将代码混淆,让破解者难以阅读。
HelloWorld 项目是怎么运行起来的
没有在AndroidManifest.xml 里注册的Activity是不能使用的。 <intent - filter> 里面的两行代码表示了 这个Activity 是这个项目的主Activity ,在手机上点击应用图标,首先启动的就是这个Activity.
AppCompatActivity是 AndroidX中提供的一种向下兼容的Activity,可以使Activity在不同系统版本中的功能保持一致。Activity 类是Android 系统提供的一个基类,我们项目中所有定义的Activity 都必须继承它或者它的子类才能拥有Activity 的特性。
Android 程序的设计讲究逻辑和视图分离,因此不推荐在Activity 中直接编写界面。通用的做法,在布局文件中编写见面,然后在Activity中引入进来。
详解项目中的资源
drawable 开头的目录是用来存放图片的
mipmap 开头的目录用来放应用图标,一般美工给我们一份图片,存放在xxhdpi目录下就好,因为这是最主流的设备分辨率目录
values开头的目录用来放字符串,样式,颜色等配置
如何使用res目录下的资源?
比如在strings.xml文件中 有一个app_name(APP名字的字符串),通常有两种方式引用
1 在代码中通过R.string.app_name 可以获得该字符串的引用
2在XML中 通过 @string/app_name可以获得该字符串的引用
同理,string部分是可以替换的,即,引用图片资源可以替换为drawable,引用应用图标可以替换为 mipmap,引用布局文件 即替换为layout,以此类推。
详解bulid.gradle文件
Android Studio 采用Gradle来构建项目。基于Groovy的特定语言DSL来进行项目配置,摒弃了传统基于XML(如Ant和Maven)的各种繁琐配置
对于最外层的build,gradle文件
两处 repositories 都声明了 google() ,jcenter() 这两行配置,就是说它们分别对应了一个代码仓库,google仓库中主要是Google自家的扩展依赖库,jcenter仓库包含的大多数第三方开源库。声明了这两个配置,就可以在项目中轻松引用任何google和jcenter仓库中的依赖库了。
dependencies闭包中使用了classpath声明了两个插件,一个Gradle插件和一个Kotlin插件。为什么要声明Gradle插件呢,因为Gradle并不是专门为构建Android项目而开发的,因此想用它来构建Android项目,必须要声明 com.android.tools.bulid:gradle:3.5.2这个插件。最后面的部分是插件的版本号,通常和当前的Android Studio的版本是对应的. 另一个Kotlin插件表示当前项目是Kotlin进行项目开发的,如果是java版本的Android 项目则不需要声明这个插件.
通常情况下,除非想要添加一些全局的项目构建配置,不然需要要修改最外层目录下的bulid.gradle文件。
app目录下的build.gradle文件
-
apply plugin (应用插件)
第一行通常有两个值可选, com.android.application 表示应用程序模块,com.android.library 表示库模块。其中应用程序模块可以直接运行,库模块只能作为代码库依附于别的应用程序模块来运行。
接下来应用 kotlin-android 和kotlin-android-extension,前者是使用kotlin开发android项目必须应用的,第二个插件帮助我们实现了一些非常好用的Kotlin扩展功能.
接着是android闭包,在闭包中可以配置项目构建的各种属性。
其中, -
compilesdkVersion
用于制定项目的编译版本, 如29表示Android 10.0系统的SDK编译 -
compilesdkVersion
用于指定项目构建工具的版本。
android闭包中嵌套了一个defaultConfig闭包,该闭包可以对项目的更多细节进行配置。其中, -
applicationId
是对每个应用的唯一标识符,绝对不能重复,默认会使用我们创建项目时指定的包名,如果想在后面对其进行修改标识符,即在这里修改。 -
minSdkVersion
用于指定项目最低兼容的Android系统版本,15表示最低兼容到Android 4.0.3系统。 -
targetSdkVersion
指定的值表示你在改目标版本上已经做过了充分测试,系统会为你的应用程序启用一些最新的功能和特性。比如Android 6.0系统(对应API水平为23)引入了运行时权限这个功能,那么将targetSdkVersion 设定成23或者更高,系统就会为你的程序启用运行时权限功能,如果设定为23以下,如22,表示系统最高只在android 5.1上做过充分测试,那么android 6.0引入的功能就不会启用。 -
versionCode
用于指定项目的版本号 -
versionName
用于指定项目的版本名
buildTypes闭包中用于指定生成安装文件的相关配置
通常只会有两个子闭包,debug和release 。debug闭包用于指定生成测试版安装文件的配置,relase闭包用于指定生成正式版安装文件的配置。 另外,debug包通常可以忽略不写。
在relase闭包中,minifyEnabled 用于指定是否对项目的代码进行混淆,true表示混淆,false表示不混淆。proguardFiles用于指定混淆时使用的规则文件。指定了两个文件,第一个proguard-android-optimize.txt 里面是所有项目通用的混淆规则,第二个proguard-rules.pro 是在当前项目的根目录下,里面可以编写当前项目特有的混淆规则。
dependencies闭包,通常 Android Studio 项目一共有3种依赖。本地依赖,库依赖和远程依赖。本地依赖可以对本地的jar包或目录添加依赖关系,库依赖可以对项目中的库模板添加依赖关系,远程依赖可以对jcenter仓库上的开源项目添加依赖关系。
- implementation filetree就是一个本地依赖声明,它表示将libs目录下所有的.jar后缀的文件都添加到项目的构建路径中。而implementation则是远程依赖声明,android.appcompat:appcompat:1.1.0就是一个标准的远程依赖库格式,其中androidx。appcompat是域名部分,用于和其他公司的库做区分;appcompat是工程名部分,用于和同一个公司中不同的库工程做区分;1.1.0是版本号,用于和同一个库不同版本做区分。 加上这句声明后,Gradle在构建项目时会首先检查一下本地是否已经有这个库的缓存,如果没有的话会自动联网下载,然后添加到项目的构建路径中。 库依赖声明的基本格式是implementation project 后面加上要依赖库的名称,比如有一个库模板的名字叫helper,那么添加这个库的依赖关系只需要添加implementation project (’:helper’)这句声明即可。
掌握日志工具的使用
Android中的日志工具类是Log(Android.util.Log) 提供了五个方法
- Log.v()
用于打印最为繁琐,意义最小的日志信息。级别是verbose - Log.d()
用于打印调试信息,对调试程序和分析问题很有帮助。级别是debug - Log.i()
打印比较重要的数据,是想看到的,可以帮你分析用户行为的数据 。级别是info - Log.w()
打印警告信息,提示程序在这个地方可能会有潜在的风险,最好去修复一下出现警告的地方。级别是warn - Log.e()
打印程序中的错误信息,当有错误信息打印出来的时候,代表程序出现严重问题,需要尽快修复。级别是error。
使用Log而不是使用println()
打印日志,使用prinln 会让日志开关不可控,不能添加日志标签,日志没有级别区分。
Logcat可以添加过滤器。Show on selected application 表示只显示当前程序的日志,Firebase是谷歌提供的一个开发者工具和基础架构平台 No Filters相当于没有过滤器。最后是 Edit Filter Configuration自定义过滤器。
Kotlin入门
变量与函数
kotlin定义一个变量只允许前面声明两种关键字 , val 和var
val (value)声明一个不可变的变量,初始赋值后就再也不能重新赋值,对应java的final变量
var(variable)声明一个可变的变量,初始赋值后仍可以重新赋值,对应java的非final变量
Kotlin的每一行代码结果不用加分号。
Kotlin的类型推导机制,比如要将一个整数赋值给变量a,那么a只能是整型变量。同理,将字符串赋值给变量a,那么a就会自动推导成字符串变量。
如果我们对一个变量延迟赋值的话,Kotlin无法自动推导它的类型,所以需要显式地声明变量类型。
比如 val a : Int = 10
显式的声明了变量a为Int类型,此时Kotlin不会再尝试进行类型推导。如果再将字符串赋值给a,那么编译器机会抛出类型不匹配的异常。
Kotlin中的Int的首字母是大写的,而java中的int首字母是小写的。代表Kotlin完全抛弃了java的基本数据类型,全部采用了对象数据类型,在java中int是关键字,而在Kotlin中Int变成了一个类,拥有自己的方法和继承结构。
Kotlin在设计的时候,提供val和var这里两种关键字,就是为了必须声明该变量是可变的,还是不可变的。 一个好的编程习惯就是,除非一个变量明确允许被修改,否则都应该给它加上不可变的关键字。
在Kotlin中,永远优先使用val来声明一个变量,当val没法满足需求时,再使用var。这样设计出来的程序将会更加健壮。
函数和方法是同一概念,只是不同语言的叫法不相同。函数是允许代码的载体,可以在一个函数编写很多行代码,当运行这个函数时函数中的代码都会全部运行。main函数是程序的入口函数,即程序一旦运行都是从main函数开始的。
fun(function =)定义函数关键字,无论什么函数,都要用fun来声明
函数的参数后面的部分可选,用于声明函数会返回什么类型,如果函数不需要返回任何数据,可以省略。
建议经常使用代码补全功能,因为使用代码补全可以帮我们自动导包。
当函数只有一行代码时(如果作用和一行代码相同的多行代码也行),我们不必写函数体,可以将唯一一行代码写在函数定义的尾部,用等号= 连接。return可以省略,因为等号可以表达返回值的意思。由于类推导机制,所以不用显式地声明函数返回值类型了。(语法糖)
举例:比较a,b两整数的大小
- 第一种写法
fun largernumber (a : Int , b : Int ) = max(a,b)
if语句
Kotlin中的if条件语句和java的if语句几乎没有区别,但是Kotlin中的if语句有个额外的功能,它可以有返回值,返回值就是if语句中每一个条件中最后一行代码的返回值
因此 可以写为
fun largernumber (a: Int, b : Int ):Int
{
return if(a>b)
{
a
} else
{
b
}
}
由于当一个函数只有一行代码时,可以省略函数体部分,用等号直接相连。虽然函数体里面代码不止一行,但作用和一行代码一样,只是返回了if语句的返回值,所以符合语法糖的使用条件,所以有第二种写法。
2. 第二种写法
fun largernumer(a : Int , b : Int ) = if(a >b) a else b
when 条件语句
Kotlin中的when语句类似于java中的switch语句,但是远比switch语句强大。
when允许传入任意类型的参数(可以是类),然后在when的结构体中定义一系列的条件,格式是
匹配值 -> {执行逻辑}
当执行逻辑只有一行时,{}可以省略。
写一个关于输入学生姓名,返回学生分数的的函数。
fun getScore(name :String) = when (name)
{
"Tom" -> 86
"jim" -> 77
"jack" ->90
else -> 0
}
when 语句和if语句一样,也可以有返回值,因此仍然可以用单行代码函数的语法糖
除了精确匹配,还可类型匹配。比如:
fun checkNumber (num : Number)
{
is Int -> println("number is Int")
is Double ->println("number is Double")
else -> println("number is support")
}
其中is关键字是匹配的核心,相当于java中的,instanceof 关键字。Number这个参数,是Kotlin内置的抽象类,向Int,Long,Double这些数字相关的类都是它的子类,所以这里可以用类型匹配来判断传入的参数到底是什么类型。
when 还有一种不带参数的用法,就是将判断的表达式完整地写在when的结构体中。注意
Kotlin中判断字符串或对象是否相等可以直接用==关键字,不像java那样调用equals方法
fun getScore2(name :String)= when
{
name.startsWith("Tom")->86 //名字开头是Tom的所有人,成绩都是86分
name =="jim"->77
name =="jack"->60
else ->0
}
循环语句
Kotlin 提供了while 循环和for循环, 其中while循环语法和技巧和JAVA完全一致,for循环和java有很大不同。
for (i in 0 ..10) println(i)
.. 是创建两端闭区间的关键字,在..的两遍指定区间的左右断点就可以创建一个区间
Kotlin中可以使用until关键字创建一个左闭右开区间,使用step关键字跳过一些元素(. .和until要求区间左端必须小于等于右端,也就是这两个关键字创建的都是升序空间)
默认情况下,for - in循环每次循环时会在区间范围内递增1,相当于java for - i循环中i++的效果,如果想跳过其中的一些元素,可以使用step关键字
使用downTo关键字创建一个降序的区间
for (i in 10 downTo 1 step 2) println(i)
Kotlin面向对象编程
封装
类是对事物的一种封装,类名通常是名词,类拥有自己的字段和函数,字段表示该类的属性,通常是名词,比如人的姓名和年龄,汽车可以拥有品牌和价格。而函数则可以表示类有哪些行为,通常是动词,比如人可以吃饭和睡觉,汽车可驾驶和保养。
Kotlin中实例化一个类的方式和java类似,不过去掉了new关键字。
继承
在Kotlin中,任何一个非抽象类默认都是不可以被继承的,相当于java中给类声明了final关键字。所以,要使类能被继承需要在类的前面加 open关键字
这么设计的原因和val差不多,类和变量组好不可变。
要让子类继承父类,在java中的关键字是extends,在Kotlin中变成了冒号 :
举例如下:
class Student : Person(){
var sno=""
var grade = 0
}
为什么要在Person后面加一对括号呢,涉及到了主构造函数和次构造函数
Kotlin将构造函数分为主构造函数和次构造函数
主构造函数是最常用的构造函数,每一个类默认都会有一个不带参数的主构造函数,当然也可以显式地给它指明参数。主构造函数的特点是没有函数体,直接定义在类名的后面即可。如下:
class Student (val sno: String,val grade :Int ):Person() {
}
这里将学号和年级两个字段放在了主构造函数中,表明对Student类进行实例化的时候,必须传入构造函数中要求的所有参数。同时,Kotlin提供init结构体,帮助我们写主构造函数中的逻辑,比如:
class Student (val sno: String,val grade :Int ):Person() {
init {
println("sno is " +sno)
println("grade is " + grade)
}
}
同时,java继承特性中有一个规定,子类的构造函数必须调用父类中的构造函数,Kotlin同样遵守。所以,子类的主构造函数调用父类中的哪个构造函数,在继承的时候通过括号来指定(如果子类没有主构造函数,继承㺑的时候也就不需要加括号了)
回看 Student类
class Student (val sno: String,val grade :Int ):Person() {
}
在这里, Person类的一对空括号表示Student类的主构造函数在初始化的时候会调用Person类的无参构造函数,即使在无参数的情况下,这对括号也不能省略。
//改造一下Person类
open class Person(val name :String ,val age :Int) {
/* var name =""
var age = 0*/
fun eat()
{
println(name +" is eating.he is " +age +" years old")
}
}
//之前Person类后面的空括号表示调用Person类的无参构造函数,现在改在了Person类,
//故在Student类中添加字段,即name和age这里两个参数,再将两个参数传递给Person类的构造函数
class Student (val sno: String,val grade :Int,name:String,age:Int ):Person(name,age) {
}
这里注意,在Student类的主构造函数中增加name和age这两个字段是,不能再将它们声明成val。因为在主构造函数中声明成val或者var的参数将自动变成该类的字段,这样就会导致和父类中的同名的name,age字段造成冲突。所以,这里的name和age参数前面我们不用加任何关键字,让它的作用域限定在主构造函数中即可。
任何一个类只能有一个主构造函数,但可以有多个次构造函数。次构造函数也可以用于实例化一个类,和主构造函数没有什么不同,不过它是有函数体的。
Ktolin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)
次构造函数通过constructor来定义,
接口
Kotlin和Java一样是单继承结构语言,任何一个类只能继承一个父类,但可以实现任意多个接口。
在JAVA中,java的继承关键字是extends,实现接口的关键是implements 。而Kotlin中统一使用冒号:
,中间用逗号进行分隔
另外,接口后面不用加上括号,因为它没有构造函数可以去调用。
在接口中定义的函数,实现接口的函数必须要全部实现。
Kotlin中 使用 override关键字来重写父类或者实现接口中的函数
fun main()
{ //创建了Student的实例,本来可以直接调用readBooks函数和doHomework函数,但是为了体现多态
// 将它传到doStudy函数中,由于Student类实现了Study接口,所以Student的实例可以传递到doStudy函数
val student = Student("Jack",19)
doStudy(student)
}
fun doStudy(study: Study)
{
study.doHomework()
study.readBooks()
}
Kotlin增加了一个额外的功能,允许对接口中定义的函数进行默认实现。如果接口中的函数拥有了函数体,这个函数体中的内容就是它的默认实现。
所以当有一个类去实现接口,只会强制要求实现没有函数体的函数,有函数体的函数可以选择是否实现,如果实现,则顶替默认实现逻辑,否则会自动使用默认的实现逻辑
Kotlin函数的可见性修饰符
Kotlin 中private修饰符和java一样,都表示只对当前类内部可见.
public修饰符作用和java一致,但Kotlin中,public 修饰符是默认项,java中default是默认项
在Kotlin中,protected关键字表示只对当前类,和子类可见。 java中表示对当前类,子类和同一路径下的类可见。
Kotlin中,抛弃了default(同一包路径下的类可见)这一概念,引入了,只对同一模块的类可见,使用的是internal修饰符
数据类和单例类
在一个规范的系统架构中,数据类通常占据着非常重要的角色,它们将服务器端或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持。
数据类通常需要重写equals,hashCode,toString这几个方法。hashCode作为equals配套方法需要一起重写。
在Kotlin中, 在一个类前面声明data关键字,表明你希望这个类是一个数据类。 Kotlin会根据主构造函数中的参数,将equals,hashcode,toString等固定且无实际逻辑意义的方法自动生成。
单例类
单例模式是最基础的设计模式之一,用于避免创建重复的对象。
在Kotlin中,使用object关键字取代clss关键字即可创建单例类
Lambda编程
集合的函数式API用来入门Lambda编程极佳,先学会创建集合
在Kotlin中,使用内置的函数listOf()初始化List集合
val list = listOf("1","2","3")
for (num in list) println(num)
不过,需要注意的是listOf()函数创建的是一个不可变的集合,所谓不可变集合就是指,该集合只能用于读取,我们无法对集合进行添加,修改,删除操纵。
在Kotlin中,使用内置的函数mutableListof()初始化集合
val list = mutableListOf("1","2","3")
list.add("4")
for (num in list) println(num)
Set集合和List集合的用法几乎一致,只是将创建集合的方式,换成了SetOf(),和mutableSetOf()函数。
需要注意的是,Set集合是不可以存放重复元素的,如果存放了多个相同元素,只会保留其中一份。
对于Map集合的用法,Map是一种键值对形式的数据结构。
在Kotlin中,不推荐使用put() ,get()方法对Map进行添加和读取数据操作。更推荐使用一种类似数组下标的数据结构,
val map = HashMap<String,Int>()
map["A"]=1
map["B"]=2
map["C"]=3
for ((letter,number) in map) println(letter+" "+number)
当然,Kotlin提供了 mapOf()函数和mutableMapof()函数简化Map的用法,在mapOf()函数中,我们可以直接传入初始化的键值对组合完成对Map集合的创建
val map = mapOf("A" to 1 ,"B" to 2, "C" to 3)
for ((leeter,number) in map) println(leeter+" " + number)
这里的键值对组合中的to不是关键字,是一个infix函数
集合的函数式API
Lamda就是一小段可以作为参数传递的代码,虽然Kotlin没有限制,但通常不建议在Lambda表达式中写太长的代码,否则可能会影响代码的可读性。
Lambda表达式的语法结构: {参数名1:参数类型,参数名2:参数类型 ->函数体}
首先最外层是一个大括号,如果有参数传入到Lambda表达式中的话,我们还需要声明参数列表,参数列表的结尾使用一个->符号,表示参数列表的结束以及函数体的开始,函数体可以编写任一行的代码(不建议太长),并且最后一行代码自动作为Lambda表达式的返回值。
需求在一个数字集合中找到最长的数字串
//常规写法
val list = listOf("1","22","333","4444")
var maxLengthNum = ""
for(num in list)if (num.length > maxLengthNum.length)maxLengthNum = num
//Lambda表达式初次写法
//maxBy就是一个普通的函数,不过接收的是一个Lambda类型的参数,并且每次遍历集合将遍历的值作为参数传递Lambda表示式.
//maxBy的工作原理是根据传入的条件来遍历集合,从而找到该条件下的最大值
val list = listOf("1","22","333","4444")
val lambda = {num:String->num.length}
val maxLengthNum = list.maxBy(lambda)
//不必专门定义一个lambda变量
val maxLengthNum = list.maxBy({num:String ->num.length})
//Kotlin规定:当Lambda参数是函数最后一个参数时,可以将Lambda表达式移到函数外面
val maxLengthNum = list.maxBy(){num:String ->num.length}
//Lambda参数是函数唯一一个参数时,可以省略括号
val maxLengthNum = list.maxBy{num:String ->num.length}
//Lambda表达式中的参数列表大多数情况下不必声明参数类型
val maxLengthNum = list.maxBy{num ->num.length}
//当Lambda表达式的参数列表中只有一个参数时,不必声明参数名,可以用it关键字代替
val maxLengthNum = list.maxBy{it.length}
集合中的map函数是最常用的一种函数式API,它用于将集合中的每个元素都映射成另外的值,映射的规则在Lambda表达式中指定,最终生成一个新的集合。
比如希望将一串字母都变成大写
//map函数功能强大,可以按照需求对集合的元素进行任意的映射转换,只需要在lambda表达式编写逻辑
val list2 = listOf("a","b","c")
val newList = list2.map({it.toUpperCase()})
for (num in newList) println(num)
//filter函数为过滤集合中的数据的函数,可以单独使用,也可以配合map函数使用
//需要注意的是,一般都是先过滤,再映射。这样,因为过滤筛选了集合元素,所以会更高效
val list = listOf("aaa","bbbb","cccccc")
val newList = list.filter { it.length<=4 }
.map { it.toUpperCase() }
for (num in newList) println(num)
Java的函数式API的使用
如果我们在Kotlin代码中调用了一个java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。Java的单抽象方法接口指的是接口中只有一个待实现方法,如果有多个,则无法使用函数式API。
//java创建并执行一个子线程(Runnable 是常见的单抽象方法接口 ,接口里面只有一个待实现的run方法)
new Thread(new Runnable
{
@Override
public void run (){
System.out.println("Thread is running")
}
}
).start()
//翻译成Kotlin为如下,这里注意Kotlin舍弃了new,所以这里用object代替new
Thread(object :Runnable{
override fun run() {
println("Thread is running")
}
}).start()
//简化 因为Runable中只有一个待实现的方法,所以即使没有显式地重写run方法,Kotlin也能明白Runable
//后面的Lambad表达式就是在run()方法中实现的内容
Thread(Runnable {
println("Thread is running")
}).start()
//如果一个java方法的参数列表有且仅有一个Java单抽象接口参数,可以将接口名省略
Thread({
println("Thread is running")
}).start()
//类似于Kotlin函数式API用法,Lambda表示是方法的最后一个参数,可以将Lambda表达式移到方法括号外面
//同时,Lambda表达式是方法唯一一个参数时,可以将方法的括号省略,最终简化如下
Thread{
println("Thread is running")
}.start()
Kotlin调用SDK接口时,常用到java函数式API
//常用的点击事件接口
public interface OnclickListener
{
void onClick(View v);
}
//java中,拥有一个按钮Button实例,使用java代码去注册这个按钮的点击事件
button.setOnclickListener(new View.OnclickListener()
{
@Override
public void onClick(view v)
{
}
})
//使用Kotlin简化
button.setOnclickListener
{
}
Kotlin的空指针检查
Kotlin默认所有的参数和变量都不可为空。
fun doStudy(study: Study)
{
study.doHomework()
study.readBooks()
}
倘若尝试向doStudy传入null参数,程序将会报错
即Kotlin将空指针异常的检查提前到编译时期,程序中如果存在空指针异常的风险,编译就会直接报错,修正后才能成功运行。
如果需要某个参数或者变量为空怎么办?Kotlin提供了可为空的类型系统,不过使用该系统时,编译时期就要解决所有潜在的空指针异常,否则无法编译通过。
可为空的系统:类名后面加一个?
比如 Int表示不可为空的整型,而Int?表示可为空的整型。
如果将希望传入的参数改成可以为空,将参数类型后面加一个?即可 ,但是要解决程序中潜在的空指针异常。 比如加一个判断处理:
fun main()
{
doStudy(null)
}
fun doStudy(study: Study?)
{
if (study!=null)
{
study.doHomework()
study.readBooks()
}
}
判空的辅助工具
?. 操作符,意思是当对象不为空时正常调用,当对象为空时什么都不做
if(a!=null) a.doSomething()
a?.doSomething()
上面两者是等价的
?:操作符,意思是 这个操作符左右两边都接收一个表达式,如果表达式的的结果不为空就返回左边表示的结果,否则返回右边表达式的结果
val c if(a!=null) a else b
val c = a?:b
上面两者等价。
编写一个函数来获得一段文本的长度,结合?. 和?: 简化函数写法
//传统写法
fun getTextLength(text:String?):Int{
if(text!=null) return text.length
return 0
}
fun getTextLength(text: String?) = text?.length?:0
上下两者等价,text可能为空,所以先使用?. 如果 text为null,则 text?.length 的值为null ,那么text?.length?:0 值将会是?:右边表达式的值也就是0,如果text不为空,则执行text的方法,计算出长度,那么 text ?.length表达式的值 也就是该长度,故 text?.length?:0的值,将会是计算出的长度。
有时候逻辑上已经将空指针处理了,但Kotlin编译器并不知道,还是会编译失败。因为有些函数,可能不知道外部已经对变量或参数进行了非空检查,那么调用改函数时,还是可能编译失败。如果我们想强行通过编译,可以使用非空断言工具!!. 去告诉Kotlin,这里的对象不会为空,让它不来做空指针检查
如下:
var content :String ?="hello"
fun main()
{
if (content!=null) printUpperCase()
}
fun printUpperCase()
{
val upperCase = content!!.toUpperCase()
println(upperCase)
}
但尽量避免使用非空断言工具!!. 因为你自信对象不会为空,可能是一个潜在空指针异常发生的时候。
使用let函数帮助空指针检查
obj.let{ obj2->
//编写具体的业务逻辑
}
调用了obj对象的let函数,然后Lambad表达式的代码机会立即执行,并且obj对象本身还会作为参数传递到Lambda表达式中。为了防止变量重名,这里参数名改成了obj2,但实际上它们是同一个对象,这就是let函数的作用。
let函数配合 ?.操作符在空指针检查的时候起到很大的作用。
fun doStudy(study: Study?)
{
study?.readBooks()
study?.doHomework()
}
//翻译为if判断语句的写法,如下就是说限制于?. 操作符,每次调用study对象都要进行一次if判断
fun doStudy(study: Study?)
{
if(study!=null) study.readBooks()
if (study!=null) study.doHomework()
}
//使用let和?.进行优化,由于?.操作符保证对象不为空,let函数会将study对象本身作为参数传递到
//Lambda表达式中,此时由于对象不为空,故可以放心调用传进来的对象的任意方法
fun doStudy(study: Study?)
{
study ?.let {stu->
stu.readBooks()
stu.doHomework()
}
}
//Lambda表达式的参数列表只有一个是,不用声明参数名。用it关键字代替。继续简化:
fun doStudy(study: Study?)
{
study ?.let {
it.readBooks()
it.doHomework()
}
}
let函数可以处理全局变量的判空问题,而if判断语句则无法做到这一点。
Kotlin中的一些重要小技巧
字符串的内嵌表达式
Kotlin内嵌表达式的语法规则
"hello, ${obj.name} . next to meet you !"
Kotlin允许字符串中 嵌入${ }这种结构的表达式,在运行时使用表达的结果替代这一部分内容。
当表达式中仅有一个变量的时候,可以将大括号省略。
//下面两种写法输出结果一致
val name = "lixiaolei"
val name2 ="shenhaojie"
println("hello, ${name+" "+name2} . next to meet you !")
println("hello, $name $name2 . next to meet you !")
//输出结果:hello, lixiaolei shenhaojie . next to meet you !
函数的默认参数值
具体来说,定义函数的时候可以给任意参数设定一个参数值,当调用此函数时就不会要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。
fun main()
{
printParams(123)
}
fun printParams(num:Int , str:String ="hello")
{
println("num is $num , str is $str")
}
当然,调用此函数时,调用方不能将错误的参数类型赋值给函数。但Kotlin中,可以根据键值对的方式传参,不必按传统写法那样按照参数定义的顺序来传参。
//示例1
fun main()
{
printParams(str = "123")
}
fun printParams(num:Int =10, str:String )
{
println("num is $num , str is $str")
}
//示例2
fun main()
{
printParams(str = "123",num =10)
}
fun printParams(num:Int , str:String )
{
println("num is $num , str is $str")
}
//定义两个次构造函数,第一个接收name,age参数,又通过this关键字调用主构造函数,并将sno和garde赋值
//第二个次构造函数不接收参数,通过this关键字调用了第一个次构造函数,并将name和age赋值
//由于第二个次构造函数间接调用了主构造函数,因此合法
//次构造函数的作用是提供使用更少的参数来对Student类进行实例化
//无参的次构造函数调用两个参数的次构造函数,两个参数的次构造函数调用4个参数的主构造函数,
//并将缺少的两个参数赋值成初始值
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){}
}
//Kotlin可以通过只写一个主构造函数,通过给参数设定默认值的方式实现上面的代码:
class Student(val sno:String="" ,val grade :Int = 0 ,name: String="", age: Int = 0)
:Person(name,age)
{
}
给主构造函数的每个参数设定初始值后,可以使用任何传参组合的方式对类进行实例化
Activity
activity是一种可以包含用户界面的组件,主要用于和用户交互。
如果需要在XML中引用一个id,就使用 @id/id_name 这种语法,
如果需要在XML中定义一个id,则需要使用@+id/id_name这种语法。
在onCreate()方法中,使用setContentView()的方法给Activity加载一个布局,由于项目中的任何资源都会在R文件中生产一个相应的资源id,因此可以通过R.layout.xx_layout 的方式得到xx_layout.xml布局的id.
所有Activity都要在AndroidManifest.xml中注册才会生效,android studio一般会默认帮我们注册。
在AndroidManifest.xml中的,
要首先启动哪个Activity就要在< activity >标签的内部加入 < intent-filter > 标签,并在标签里面添加:
< action android:name=“android.intent.action.MAIN”/>
< category android:name=“android.intent.category.LAUNCHER”/>
这两句声明即可。
android.intent.action.MAIN决定应用程序最先启动的是哪个Activity android.intent.category.LAUNCHER决定应用程序是否显示在程序列表里
Activity窗口的组成图
任何一个Activity中显示的界面主要都由两部分组成,标题栏和内容布局。标题栏就是在很多界面顶部显示的那部分内容,比如刚刚我们的那个例子当中就有标题栏,可以在代码中控制让它是否显示。而内容布局就是一个FrameLayout,这个布局的id叫作content,我们调用setContentView()方法时所传入的布局其实就是放到这个FrameLayout中的,这也是为什么这个方法名叫作setContentView(),而不是叫setView()。
Toast的使用
setContentView(R.layout.first_layout)
val button1 : Button = findViewById(R.id.button1)
button1.setOnClickListener {
Toast.makeText(this,"you clicked Button1",Toast.LENGTH_SHORT).show()
}
在Activity中,可以通过findViewById()方法获取在布局文件中定义的元素,findViewById()方法返回是一个继承自View的泛型对象,所以Kotlin无法自动推导它返回的是什么控件,所以在实例化的时候,需要将Button或者其他控件显式地声明。得到控件实例后,可以调用setOnClickListener()方法为控件注册一个监听器,点击控件就会执行onClick()方法。注意:由于点击事件接口OnClickListerner是一个单抽象方法接口,只有onClick方法,所以可以使用函数式API的写法来简化代码
Toast的makeTest()方法有3个参数,第一个参数是Context,也就是Toast要求的上下文,由于Activity本身就是一个Context对象,所以一般传入this即可。第二个参数是Toast显示的文本内容。第三个参数是Toast显示的时长,有两个内置常量可以选择。
在Kotlin中, kotlin.android.extensions插件可以根据布局文件定义的控件id,自动生成一个具有相同名称的变量,我们可以直接在Activity中使用这个变量,而不是再调用findViewById()方法。
//这里的button1是布局文件中定义的一个控件id
button1.setOnClickListener {
Toast.makeText(this,"hello",Toast.LENGTH_SHORT).show()
}
Menu
首先在res目录下新建一个menu文件夹,在文件夹下新建菜单文件。
< item>标签用来创捷具体的某一个菜单项 通过android:id属性给菜单项指定唯一的标识符 ,通国android:title属性给菜单项指定一个名称
通过重写onCreateOptionsMenu()方法显示菜单
通过重写onOptionsItemSelected()方法定义菜单响应事件
重写onCreateOptionsMenu()方法
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main,menu)
return true
}
讲解这段代码时,先看java的一个语法糖,在Kotlin中可以使用如下代码来设置和读取Book类中的pages字段
public class Book
{
private int pages;
public int getPages()
{
return pages;
}
public void setPages(int pages)
{
this.pages = pages;
}
}
val book = Book()
book.pages = 500
val booPages = book.pages
这里看上去没有调用Book类中的setPages()和getPages()方法,而是直接对pages字段进行赋值。但实际是Kotlin提供的语法糖,它会在后面自动将上述的代码转换成调用setPages()方法和getPages()方法。
上面在onCreateOptionsMenu()中编写的menuInflater()就使用了这种语法糖,实际上调用了父类的getMenuInflater()方法。getMenuInflater()方法能得到一个MenuInflater对象,再调用它的inflate()方法,就可以为Activity创建菜单了。
inflate()方法接收两个参数:第一个参数是我们通过哪一个资源文件来创建菜单,一般是res目录中menu文件下的资源文件。第二个参数是用于指定我们的菜单项添加到哪一个Menu对象当中,一般直接使用onCreateOptionsMenu()方法中传进来的menu参数。
最后,由于方法是Boolean,所以要返回true,如果返回false,创建的菜单将无法显示。
重写onOptionsItemSelected()方法来定义菜单响应事件:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId)
{
R.id.add_item ->Toast.makeText(this,"Clicked Add!",Toast.LENGTH_SHORT)
.show()
R.id.remove_item ->Toast.makeText(this,"Clicked Remove!",Toast.LENGTH_SHORT)
.show()
}
return true
}
我们通过调用item.itemId 来判断点击的是哪一个菜单项。这里用了语法糖,Kotlin实际上在背后调用的是item的getItemId()方法。
菜单里的菜单默认是不显示的,只有点击菜单按钮才会弹出具体的内容,因此不会占用任何Activity的空间。
在Activity类中,直接调用finish()方法即可销毁当前的Activity.
button1.setOnClickListener {
finish()
}
使用Intent在Acitivity之间穿梭
Intent 是Android 程序中各组件之间进行交互的一种重要方式,不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。
Intent一般分为显式Intent和隐式Intent。
显式Intent
Intent有多个构造函数的重载,其中一个是Intent(Context packageContext,Class<?>cls) 这个构造函数接收两个参数:第一个参数Context要求提供一个启动Activity的上下文;第二个是参数Class用于指定想要启动的目标Activity,通过这个构造函数可以构建出Intent的“意图”,Intent的“意图”十分明显,所以我们称为显示Intent。
示例如下:
button1.setOnClickListener {
val intent = Intent(this,SecondActivity::class.java)
startActivity(intent)
}
首先构建了一个Intent对象,第一个参数传入this也就是这里Activity本身作为上下文,第二个参数传递SecondActivity::class.java作为目标Activity。意图明显,也就是在FirstActivity的基础上打开SecondActivity. 注意Kotlin中SecondActivity::class.java相当于Java中的SecondActivity.class的写法。
隐式Intent
<activity android:name=".SecondActivity">
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
在< action>标签中指明了当前的Activity可以响应com.example.activitytest.ACTION_START这个action,而< category>标签中包含了一些附加信息,更精确的指明了Activity能够响应的Intent中还可能带有的category, 只有< action>和< category>同时匹配Intent中的action和category时,这个Activity才能响应这个Intent.
为什么加入android.intent.category.DEFAULT?它的 意思是说,每一个通过 startActivity() 方法发出的隐式 Intent 都至少有一个 category,就是 “android.intent.category.DEFAULT”。 所以只要是想接收一个隐式 Intent 的 Activity 都应该包括 “android.intent.category.DEFAULT” category,不然将导致 Intent 匹配失败。
button1.setOnClickListener {
val intent = Intent("com.example.activitytest.ACTION_START")
startActivity(intent)
}
使用了Intent的另一个构造函数,直接将action的字符串传了进去,表明我们想要启动能够响应com.example.activitytest.ACTION_START的Activity。 这里虽然没有明确指定category,却能和前面的SecondActivity响应是因为,android.intent.category.DEFAULT是一种默认的category,调用 startActivity方法时,会自动将这个category添加到Intent中去。
每个Intent中只能指定一个action,但能指定多个category。
由于只有< action>和< category>必须同时匹配Intent中的action和category时,Activity才能响应Intent。故我们尝试新增一个category。
button1.setOnClickListener {
val intent = Intent("com.example.activitytest.ACTION_START")
intent.addCategory("com.example.activitytest.My_CATEGORY")
startActivity(intent)
}
那么为了保证Activity能响应intent,我们必须在响应Activity中添加新的category声明
<activity android:name=".SecondActivity">
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="com.example.activitytest.My_CATEGORY"/>
</intent-filter>
</activity>
这样,Intent的category和action内容同时和响应Activity的category和action匹配,故响应Activity成功接收Intent。
得出结论:
1、一个 Intent 可以有多个 category,但至少会有一个,也是默认的一个 category。
2、只有 Intent 的所有 category 都匹配上,Activity 才会接收这个 Intent。
更多的隐式Intent用法
使用隐式Intent不仅可以启动自己程序内的Activity,还可以启动其他程序的Activity,这样就使多个应用程序之间的功能共享成为了可能。
如果想展示一个网页们只需要调用系统的浏览器来打开网页即可。
button1.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data= Uri.parse("https://www.baidu.com")
startActivity(intent)
}
首先指定了Intent的action是 Intent.ACTION_VIEW,这是Android系统的内置动作,其常量值为android.intent.action.VIEW. 然后通过 Uri.parse()方法将一个网址字符串解析为一个Uri对象,再调用Intent的setData()方法将这个Uri对象传递进去。 (这里使用了语法糖,看上去好像直接给Intent的data属性赋值一样) SetData()方法并不复杂,它会接收一个Uri对象,主要用于指定当前Intent正在操作的数据,而这些数据通常是以字符串的形式传入Uri.parse()方法中解析产生的。
可以在< intent-filter>标签中再添加一个< data>标签,用于更精确地指定当前Activity能够响应的数据。
< data>标签中主要可以配置以下内容:
android:scheme 用于指定数据的协议部分,如上例中的http部分 android:host
用于指定数据的主机名部分,如上例中的www.baidu.com部分 android:port
用于指定数据的端口部分,一般紧随在主机名之后 android:path
用于指定数据的主机名和端口之后的部分,如一段网址中跟在域名之后的内容 android:mimeType
用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
只有< data>标签中指定的内容和Intent中携带的Data完全一致时,当前Activity才能够响应该Intent。不过在< data>标签中一般不会指定过多的内容。
新建一个ThirdActivity,并在Android.Manifest.xml文件中修改ThirdActivity的注册
//ThridActivity中添加一个按钮作为布局
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="button 3"/>
//修改ThirdActivity的注册,使其能够响应一个打开网页的Intent
<activity android:name=".ThirdActivity">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="https"/>
</intent-filter>
</activity>
这个ThirdActivity能够响应的action是 intent.action.VIEW的常量值,category则是默认的category值,另外,指定了数据的协议部分必须是https协议。这样ThirdActivity就能像浏览器一样,能够响应一个打开网页的Intent了
注意,Android studio认为所有能够响应ACTION_VIEW的Activity都应该加上BROWSATBL的category.
即在< intent-filter> 添加 < category android:name=“android.intent.category.BROWSABLE”/>否则就会忽略。加入BROWSABLE的category是为了 实现deep link功能,如果不加入,则需要在< intent-filter>标签上使用 tools:ignore 属性将其警告忽略.
运行程序后,系统将会自动弹出一个列表,显示了目前能够响应这个Intent的所有程序。
向下一个Activity传递数据
在启动Activity传递数据的思路很简单,Intent中提供了一系列putExtra()方法重载,可以把数据暂存在Intent中,在启动另一个Activity的时候,再把这些数据从Intent中取出即可
比如现在想把FirstActivity中的一个字符串传递到SecondActivity中:
button1.setOnClickListener {
val data = "hello SecondActivity"
val intent = Intent(this,SecondActivity::class.java)
intent.putExtra("extra_data",data)
startActivity(intent)
}
这里通过显式Intent的方式启动SecondActivity,通过putExtra的范式传递了一个字符串。注意,这里putExtra方法传递两个参数,第一个是键,用于之后从Intent中取值,第二个参数才是真正要传递的数据。
然后在SecondActivity中将传递的数据取出,并打印:
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
val extraData = intent.getStringExtra("extra_data")
Log.d("SecondActiviy", "extra data is $extraData")
}
}
上面代码中的intent实际上调用的是父类中的getIntent方法,该方法会获取用于启动SecondActivity的Intent,然后调用getStringExtra()方法传入相应的键值,就可以得到传递的数据了。由于这里我们传入是字符串,所以使用getStringExtra方法。如果我们传递的是整型数据,那么使用getIntExtra()方法,以此类推。
返回数据给上一个Activity
数据可以从Activity 返回给上一个Activity,Activity类中有一个用于启动Activity的 startActivityForResult() 方法,它的期望在Activity销毁的时候能够返回一个结果给上一个Activity。
startActivityForResult() 有两个参数,第一个参数是Intent,第二个是请求码,用于在之后回调中判断数据的来源。
button1.setOnClickListener {
val intent =Intent (this,SecondActivity::class.java)
startActivityForResult(intent,1)
}
这里使用了startActivityForResult()方法来启动SecondActivity,请求码只要是唯一值即可,这里用 1作为请求码。 注意 startActivityForResult()方法 是Activity的启动方法,所以不再需要使用startActivity 作为启动方法,重复使用启动方法将会产生bug。
在SecondActivity注册点击事件:
button2.setOnClickListener {
val intent = Intent()
intent.putExtra("data_return","hello FirstActivity")
setResult(Activity.RESULT_OK,intent)
finish()
}
//我们构建了一个Intent,不过这个Intent,仅仅用于传递数据,它没有任何“意图”。紧接着把要传递的数据存放在Intent中,然后调用了 setResult()方法。 这个方法很重要,专门用于向上一个Activity返回数据。setResult()方法接收两个参数:第一个参数用于向上一个Activity返回处理结果,一般只使用RESULT_OK或者RESULT_CANCELED这里两个值,第二个参数则把带有数据的Intent传递回去。最后调用finish方法来销毁当前的Activity。
由于我们使用的是startActivityForResult()方法来启动SecondActivity的,在SeconodActivity被销毁的时候会调用上一个Activity的onActivityResult()方法,所以我们要在FirstActivity中重写这个方法,得到返回的数据。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when(requestCode)
{
1->if (resultCode== Activity.RESULT_OK)
{
val returnData = data?.getStringExtra("data_return")
Log.d("FirstActivity","returned data is $returnData")
}
}
}
onActivityResult()有3个参数,第一个参数是requestCode,即启动Activity传入的请求码,第二个参数resultCode 即我们返回数据时传入的处理结果,第三个参数 data, 即携带返回数据的Intent。 由于在一个Activity中有可能调用startActivityForResult()去启动很多不同的Activity,而每一个Activity返回的数据都会回调到 onActivityResult()这个方法中,所以要通过检查requestCode判断数据来源,确定数据是SecondActivity返回的之后,再通过resultCode判断处理结果是否成功。最后再从data中取值。(这里的data是指携带返回数据的Intent)
打个比方,Intent好比一个箱子,若想把箱子里面的东西,从第一个坑放到第二个坑,那么只需要开始在第一个坑中把东西装好,然后进入第二个坑,把东西放下也就是把数据传递好了。
但如果想要把数据返回,由于第一个坑 可能会把东西送往不同的坑那么开始的时候,就必须在第一个坑中贴好标签,也就是请求码。那么我想收回东西的时候,先是准备一个新的箱子 装我带回来的东西,装好东西后,把新箱子送回第一个坑。对照标签,然后知道是哪个坑送回的东西。这样完成了返回的工作。
如果用户在SecondActivity中并不是通过点击按钮,而是按下Back键回到FirstActivity,那么我们就必须重写SecondActivity中的 onBackPressed()方法来解决问题。
override fun onBackPressed() {
//注意 这里删除了super.onBackPressed()方法
//super.onBackPressed()是执行系统默认的操作,就是退出当前Activity,
//所以当我们重新这个方法时,不加super.onBackPressed(),就可以不退出Activity,执行自己的代码。
val intent = Intent()
intent.putExtra("data_return","hello FirstActivity")
setResult(Activity.RESULT_OK,intent)
finish()
}
这样,当用户按下Back键,就会执行onBackPressed()方法,将数据存储到Intent中,这样就能返回数据了。