文章目录


第一章

1.3.5 详解项目中的资源

drawable 放图片
mipmap 放图标

drawable 我们应该自己创建drawable-hdip、drawable-xhdpi、drawable-xxdpi。当然这只是理想情况,更多的时候美工指挥提供给我们一份图片,这时可以把所有图片放到 drawable-xxhdpi 目录下,因为这是最主流的设备分辨率目录

1.3.6 详解build.gradle文件

我们新建的 kotlin 项目,有两个 build.gradle,先看外层的

buildscript {
ext.kotlin_version = "1.3.72"
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.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()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

首先,两处​​repositories​​​的闭包中都声明了​​google()​​​和​​jcenter()​​这两行配置,其实它们分别对应了一个代码仓库,google 仓库中包含的主要是 Google 自家 的扩展依赖库,而 jcenter 仓库中包含的大多是些第三方的开源库。声明了这两行配置之后, 我们就可以在项目中轻松引用任何 google 和 jcenter 仓库中的依赖库了

接下来​​dependencies​​​ 中使用 ​​classpath​​​声明了两个插件:一个 Gradle 插件和一个 Kolin 插件。为什么要声明 Gradle 插件呢? 因为 Gradle 并不是专门为构建Android 项目而开发的。Java、C++等很多种项目也可以使用Gradle来构建,此如果我们想用它来构建 Android 项目,则需要声明 ​​com.android.tools.build:gradle:4.0.1​​​ 这个插件。其中,最后面的部分是插件的版本 号,它通常和当前 Android Studio 的版本是对应的,比如我现在使用的是 Android Studio 4.0.1 版 本,那么这里的插件版本号就应该是4.0.1
而另外一个Kotlin 插件则表示当前项目是使用 Kotlin 进行开发的,如果是 Java 版的Android项目,则不需要声明这个插件

通常情况下,你并不需要修改这 个文件中的内容,除非你想添加一些全局的项目构建配置

下面看 app 下的build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 29
buildToolsVersion "29.0.3"

defaultConfig {
applicationId "com.xx.kotlinapplication"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

}

首先第一行应用了一个插件,一般有两种值可选:​​com. android.application​​​表示这是一个应用程序模块, ​​com. android.library​​表示这是一个库模块。二者最大的区别在于,应用程序模块是可以直接运行的,库模块只能作为代码库依附于别的应用程序模块来运行

接下来的两行应用了​​kotlin-android​​​ 和​​kotlin-android-extensions​​这两个插件。如果你想要使用 Kotlin 来开发Android项目,那么第一个插件就是必须应用的。而第二个插件帮助我们实现了一些非常好用的Kotlin扩展功能

紧接着是一个大的​​android​​​闭包,在这个闭包中我们可以配置项目构建的各种属性。其中, ​​compilesdkVersion​​​用于指定项目的编译版本,这里指定成29表示使用 Android 10.0 系统的 SDK编译。​​buildToolsversion​​​用于指定项目构建工具的版本,目前最新的版本就是29.0.3, 如果有更新的版本时,Android Studio会进行提示
​​​android​​​ 闭包中又嵌套了一个​​defaultConfig​​​闭包,​​defaultConfig​​​ 闭包 中可以对项目的更多细节进行配置。其中,​​applicationId​​​ 是每一个应用的唯一标识符, 绝对不能重复,默认会使用我们在创建项目时指定的包名,如果你想在后面对其进行修改,那么就是在这里修改的。关于 ​​minSdkVersion​​​ 、 ​​targetSdkVersion​​​ 的含义和区别可查看 ​​minSdkVersion、compileSdkVersion、targetSdkVersion​​​。​​versionCode​​​ 用于指定项目的版本号,​​versionName​​​ 用于指定项目的版本名
​​​testInstrumentationRunner​​​ 用于在当前项目中启用JUnit 测 试,你可以为当前项目编写测试用例,以保证功能的正确性和稳定性
​​​buildTypes​​​ 闭包 中用于指定生成安装文件的相关配置,通常只会有两个子闭包:一个是debug,一个是release。debug闭包是可以忽略不写的。​​minifyEnabled​​​用于指定是否对项目的代码进行混淆。​​proguardFiles​​​ 用于指定混淆时使用的规则文件,这里指定了两个文件:第一个proguard-android-optimize.txt是在​​<Android SDK>/tools/proguard​​​ 目录下的,里面是所有项目通用的混淆规则;第二个proguard-rules.pro是在当前项目的根目录下的,里面可以编写当前项目特有的混淆规则。需要注意的是,通过Android Studio直接运行项 目生成的都是测试版安装文件
​​​dependencies​​闭包可以指定当前项目所有的依赖关系。通常Android Studio项目共有3种依赖 方式:本地依赖、库依赖和远程依赖。本地依赖可以对本地的 jar 包或目录添加依赖关系,库依赖可以对项目中的库模块添加依赖关系,远程依赖则可以对 jcenter 仓库上的开源项目添加依赖关系

​dendencie​​​闭包中,第一行的​​implementation fileTree​​​就是个本地 依赖声明,它表示将libs目录下所有后缀为 .jar 的文件都添加到项目的构建路径中
而​​​implementaton​​​ 则是远程依赖声明,​​implementation 'androidx.appcompat:appcompat:1.2.0'​​​ 就是一个标准的远程依赖库格式, 其中​​androidx.appcompat​​​是域名部分,用于和其他公司的库做区分;​​appcompat​​​是工程名部分. 用于和同一个公司中不同的库工程做区分;​​1.2.0​​​是版本号,用于和同一个库不同的版本做区分。 加上这句声明后,Gradle在构建项目时会首先检查下本地是否已经有这个库的缓存,如果没有的 话则会自动联网下载,然后再添加到项目的构建路径中
至于库依赖声明这里没有用到,它的基本格式是​​​implementation project​​​后面加上要依赖的库的名称,比如有一个库 模块的名字叫 helper, 那么添加这个库的依赖关系只需要加入​​implementation project(':helper)​​​这句 声明即可
​​​testImplementation​​​ 和​​androidTestImplementation​​都是用于声明测试用例库的,这个我们暂时用不到,先忽略

第二章

2.1 kotlin语言简介

编程语言大致可分为两种:编译型语言和解释型语言。编译型语言的特点是编译器会将我们编写的源代码一次性编译成计算机可识别的二进制文件,然后计算机直接执行,像C和C++都属于编译型语言。解释型语言则完全不一样,它有一个解释器,在程序运行时,解释器会一行行地读取我们编写 的源代码,然后实时地将这些源代码解释成计算机可识别的二进制数据后再执行,因此解释型语言通常效率会差一些,像Python和JavaScript都属于解释型语言

那Java 是属于编译型语言还是解释型语言呢?对于这个问 题,即使是做了很多年Java开发的人也可能会答错。有Java 编程经验的人或许会说,Java 代码
肯定是要先编译再运行的,初学Java的时候都用过javac这个编译命令,因此Java属于编译型语 言。如果这也是你的答案的话,那么恭喜你,答错了!虽然Java代码确实是要先编译再运行的, 但是Java代码编译之后生成的并不是计算机可识别的二进制文件,而是一种特殊的​​​class​​​文件, 这种​​class​​​文件只有Java 虚拟机( Android中叫ART,一种移动优化版的虚拟机)才能识别,而 这个Java虚拟机担当的其实就是解释器的角色,它会在程序运行时将编译后的​​class​​文件解释成计算机可识别的二进制数据后再执行, 因此,准确来讲,Java 属于解释型语言

其实Java虚拟机并不直接和 你编写的java代码打交道,而是和编译之后生成的​​class​​​文件打交道。那么如果我开发了一门新的编程语言。然后自己做了个编译器,让它将这门新语言的代码编译成同样规格的​​class​​文件,java 虚拟机能不能识别呢?设错,这其实就是Kolin的工作原理了

kotlin语法简洁、语法更高级,而且它和 java 100%兼容,可以直接调用 java 编写的代码,而且可以无缝使用 java 第三方开源库

2.2 如何运行kotlin代码

第一种办法是下载 IntelliJ IDEA,具体可以看我之前的 ​​Kotlin教程​​​ 第二种办法是​​在线运行kotlin​​,翻墙之后就快点了
第三种办法就是Android Studio,我们可以创建一个安卓项目,然后编写一个 kotlin 的 main() 函数就可以独立运行 kotlin 代码了

我们使用第三种方法,新建一个Kotlin项目,和平时新建 Android 项目一样,不过语言选择 kotlin
然后 MainActivity 同级新建 一个 LearnKotlin 文件

《第一行代码》第三版笔记_ide
《第一行代码》第三版笔记_java_02
在这个文件编写一个 main 函数,点击左侧绿色按钮即可运行
《第一行代码》第三版笔记_第一行代码_03

2.7.2 判空辅助工具

通常的判空代码

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

我们写这样一段代码,定义了一个可以为空的全局变量 content,然后在 main() 函数中 content 不为空时调用 printUpperCase()。结果发现 printUpperCase() 认为存在空指针风险,编译不通过
《第一行代码》第三版笔记_第一行代码_04
我们可以使用非空断言工具,告诉 Kotlin,我非常确信这里不会为空,不用做空指针检查了

fun printUpperCase(){
val upperCase = content!!.toUpperCase()
println(upperCase)
}

这样写并不好,其实是一个潜在的空指针异常

最后再学习一个辅助工具 ​​let​​。这是一个函数,这个函数提供了函数式 API 编程接口,并将原始调用对象作为参数传递到 Lambda 表达式中

obj.let{obj2 ->
//编写具体业务逻辑
}

obj 对象调用了 let 函数,Lambda 表达式中代码被执行,obj 对象会作为参数传递到 Lambda 表达式中。不过为了防止变量重名,这里将参数改成了 obj2,实际上它们是同一个对象。先看一段代码:

fun doStudy(study:Study?){
study?.readBooks()
study?.doHomework()
}

​doStudy()​​​ 这个方法传入的 study 可以为空,然后在方法内再进行判断:如果 study 不为空执行​​study.readBooks()​​​,如果为空不进行任何操作;如果 study 不为空执行 ​​study.doHomework()​​,如果为空不进行任何操作

这样写是有点啰嗦的,受制于 ​​?.​​​ 我们判断了两遍 study 是否为空,这时候我们使用 ​​let​​ 进行优化

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

对象不为空调用 ​​let​​​ 函数。​​let​​​ 会将 study 本身作为参数传到 Lambda 表达式中。当 Lambda 表达式的参数列表只有1个参数时,可直接使用 ​​it​​代替,所以简化为:

fun doStudy(study:Study?){
study?.let{
it.readBooks()
it.doHomework()
}
}

最后一点:​​let​​​ 函数可以处理全局变量判空问题,​​if​​​ 不行。全局变量的值随时可能被其他线城修改,即使做了判空处理也无法保证 study 变量空指针风险。这一点上就体现了 ​​let​​​ 函数的优势
《第一行代码》第三版笔记_第一行代码_05

第三章

3.2.4 在 Activity 中使用 Toast

点击布局中的按钮,用 Toast 弹出 “Hello”

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button:Button = findViewById(R.id.button)
button.setOnClickListener{ Toast.makeText(this,"Hello",Toast.LENGTH_SHORT).show()}
}
}

使用 Kotlin编写的 Android 项目在 ​​build.gradle​​​ 文件头部默认引入了一个 ​​kotlin-android-extensions​​​ 插件,这个插件会根据布局文件中定义的控件 id 自动生成一个具有相同名称的变量,在Activity 中可以直接使用这个变量,因此我们不用再写 ​​findViewById​​了:

《第一行代码》第三版笔记_ide_06

3.2.5 在 Activity 中使用 Menu

在 menu 下增加 main.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/add_item"
android:title="Add" />
<item
android:id="@+id/remove_item"
android:title="Remove" />
</menu>
在这里插入代码片override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main,menu)
return true
}

我们在使用 ​​menuInflater​​​时,实际上是调用了父类的 ​​getMenuInflater()​​​方法。 ​​getMenuInflater()​​方法能得到一个 MenuInflater 对象。这是 Kotlin 给我们的语法糖。最后这个方法返回 true,表示允许将这个菜单显示出来,如果返回 false ,创建的菜单将无法显示

重写 ​​onOptionsItemSelected​​方法

override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId){
R.id.add_item -> Toast.makeText(this,"You clicked Add",Toast.LENGTH_SHORT).show()
R.id.remove_item -> Toast.makeText(this,"You clicked Remove",Toast.LENGTH_SHORT).show()
}
return true
}

这里的 ​​item.itemId​​​ 也用到了刚才的语法糖,背后调用 item 的 ​​getItemId()​​ 方法

《第一行代码》第三版笔记_android_07

3.3.1 使用显式 Intent

打开 SecondActivity

val intent = Intent(this,SecondActivity::class.java)
startActivity(intent)

注意:Kotlin 中 SecondActivity::class.java 的写法就相当于 Java 中 SecondActivity.class 的写法

3.3.2 使用隐式 Intent

打开网页

val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://www.baidu.com")
startActivity(intent)

3.4.5 Activity被回收了怎么办

Activity 中提供了一个 ​​onSaveInstanceState()​​ 回调方法,这个方法保证在 Activity 被回收之前一定会被调用

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val tempData = "Something you just typed"
outState.putString("data_key",tempData)
}

在 ​​onCreate()​​​方法中也有一个 Bundle 类型的参数,这个参数一般情况下都是 null,但是如果在 Activity 被系统回收之前,你通过 ​​onSaveInstanceState()​​ 方法保存了数据,这个参数就会有之前保存的全部数据

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

第四章

4.2.3 EditText

点击 Button 获取 EditText 内容,其中 ​​editText.text​​ 实际上调用的是 EditText 的 getText() 方法

override fun onClick(v: View?) {
when(v?.id){
R.id.button -> {
val inputText = editText.text.toString()
Toast.makeText(this,inputText,Toast.LENGTH_SHORT).show()
}
}
}

4.2.5 ProgressBar

id 为 progressBar 的 ProgressBar 控件进度条修改

progressBar.progress += 10

4.2.6 AlertDialog

《第一行代码》第三版笔记_android_08

AlertDialog.Builder(this).apply {
setTitle("This is Dialog")
setMessage("Something important")
setCancelable(true)
setPositiveButton("OK"){dialog, which ->

}
setNegativeButton("Cancel"){dialog, which ->

}
show()
}

4.4 系统控件不够用?创建自定义控件

《第一行代码》第三版笔记_第一行代码_09
我们用到的所有控件都是直接或间接继承自 View 的,所有布局都是直接或间接继承自 ViewGroup 的

4.4.1 引入布局

将系统自带的标题栏隐藏掉

supportActionBar?.hide()

4.4.2 创建自定义控件

《第一行代码》第三版笔记_java_10
title.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<Button
android:id="@+id/btn_back"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_margin="8dp"
android:background="@drawable/back" />

<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="Title" />

<Button
android:id="@+id/btn_edit"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_margin="8dp"
android:background="@drawable/edit" />
</LinearLayout>

TitleLayout

class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
init{
LayoutInflater.from(context).inflate(R.layout.title,this)
btn_back.setOnClickListener {
val activity = context as Activity
activity.finish()
}
btn_edit.setOnClickListener {
Toast.makeText(context,"U clicked edit",Toast.LENGTH_SHORT).show()
}
}
}

布局中使用

<com.xx.kotlinapplication.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

4.7.2 编写精美的聊天界面

写完界面,定义一个消息的实体类

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

Msg 类中只有两个字段,content 表示消息内容,type 表示消息类型(收到或发出):TYPE_RECEIVED 或 TYPE_SEND。我们定义成了常量,使用的关键字是 ​​const​​​,注意只有在单例类、companion object 或 顶层方法中才能使用 ​​const​​关键字