Kotlin

类class

定义

1
2
3
4
5
6
7
8
open class Person {//open:可被继承,一个类默认(无open)不可被继承
var name = ""
var age = 0 //name,age两个字段,表示该类所拥有的属性

fun eat() {
println(name + " is eating. He is " + age + " years old.")
}//函数:表示该类可以有那些行为
}

实例化

1
2
3
4
5
6
fun main() {
val p = Person()//实例化
p.name = "Jack"
p.age = 19
p.eat()
}

继承

1
2
3
4
5
6
7
8
9
10
class Student(val sno:String, val grade:Int):Person() {//Person后的括号代表Student类的主构造函数在初始化时会调用Person类的无参数构造函数
//var sno = ""
//var grade = 0
init {
println("sno is " + sno)
println("grade is " + grade)
}//init里写主构造函数的逻辑
}

val studend = Student("a123",5)//实例化时需要传入对应的参数

接口

1
2
3
4
interface Study {
fun readBooks()
fun doHomework()//接口中函数不要求有函数体
}
1
2
3
4
5
6
7
8
9
10
class Student(name:String, age:Int) : Person(name,age), Student {
//Student类继承了Person类,同时实现了Student接口
override fun readBooks() {
println(name + " is reading.")
}
override fun doHomework() {
println(name + " is doing homework.")
}
//override:重写父类或者实现接口中的函数
}

在main()函数中调用这两个接口中的函数:

1
2
3
4
5
6
7
8
fun main() {
val student = Student("Jack",19)
doStudy(student)
}
fun doStudy(study: Study) {
study.readBooks()
study.doHomework()
}

对接口中定义的函数进行默认实现:

1
2
3
4
5
6
interface Study {
fun readBooks()
fun doHomework() {
println("do homework default implementation.")
}
}//此时一个类实现该接口时只强制要求实现readBooks()函数,doHomework()函数不强制要求。而当类没有实现doHomework()函数时,则会自动使用接口中定义的默认实现逻辑

数据类

1
2
data class Cellphone(val brand: String, val price: Double)
//声明data关键字后,Kotlin会自动生成equals()、hashCode()、toString()等方法

单例类

1
2
3
4
5
6
7
8
//创建单例类:
object Singleton {
fun singletonTest(){
println("singletonTest is called.")
}
}
//调用单例类中的函数:
Singleton.singletonTest()

Lambda编程

集合的创建与遍历

1
2
3
4
5
6
val list = listOf("Apple","Banana","Orange","Pear","Grape")//不可变集合
val list = nutablelistOf("Apple","Banana","Orange","Pear","Grape")//可变集合
list.add("Watermelon")
for (fruit in list) {
println(fruit)
}

set集合与list集合类似,不过set集合不能存放重复的元素,如果存放,则只会保留其中的一份

map集合:Map是一种键值对形式的数据结构

1
2
3
4
5
6
7
fun main() {
val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)
//定义map集合并赋值,其中to是一个infix函数
for ((fruit,number) in map) {
println("fruit is " + fruit , "number is " + number)
}
}

集合的函数式API

Java函数式API的使用

空指针检查

可空类型系统

定义:在类名后加上?。如Int表示不可空的整型,Int?表示可空的整型。

Kotlin默认参数和变量都不可空

判空辅助工具

  • ?.:当对象不为空时调用相应函数。

    1
    2
    3
    if (a != null) {
    a.doSomething()
    }

    ↓↓↓↓↓↓↓↓↓↓↓↓

    1
    a?.doSomething()
  • ?::左右两边各接收一个表达式,如果左边不为空则返回左边表达式,反之返回右边表达式。

    1
    2
    3
    4
    5
    6
    fun getTextLength(text: String?): Int {
    if (text != null) {
    return text.length
    }
    return 0
    }

    ↓↓↓↓↓↓↓↓↓↓↓↓

    1
    fun getTextLength(text: String?): text?.length ?: 0

非空断言工具

!!:在对象后加上!!意在告诉Kotlin,我非常确信这里的对象不会为空,所以不用你来帮我做空指针检查了,如果出现问题,你可以直接抛出空指针异常,后果由我自己承担。

let函数

1
2
3
obj.let { obj2 ->
//编写具体的业务逻辑
}//obj2与obj是同一个对象
1
2
3
4
fun doStudy(study:Study?) {
study?.readBooks()
study?.doHomework()
}

↓↓↓↓↓↓↓↓↓↓↓↓

1
2
3
4
5
6
fun doStudy(study:Study?) {
study?.let {
it.readBooks()//只有一个参数时可以不声明参数名,直接用it代替
it.doHomework()
}//let函数可处理全局变量的判空问题,if则不行
}

小技巧

字符串内嵌表达式

1
2
3
val brand = "Samsung"
val price = "1299.99"
println("Callphone(brand=$brand, price=$price)")

函数的参数默认值

1
2
3
4
5
6
fun printParams(num:Int = 100, str:String) {
println("num is $num, str is $str")
}
fun main() {
printParams(str = "world")//顺序无所谓
}

↓↓↓↓↓↓↓↓↓↓↓↓

1
num is 100, str is world

活动Activity

创建Acitvity

  1. 新建项目,选择No Activity

  2. 右键app/src/main/java/com.example.(activitytest)NewActivityEmpty Activity

    1. 勾选Generate Layout File表示会自动为FirstActivity创建一个对应的布局文件
    2. 勾选Launcher Activity表示会自动将FirstActivity设置为当前项目的主活动(步骤6)
  3. 右键app/src/main/resNewDirectory(目录)命名为layout

  4. 右键layoutNewLayout resource file

    布局文件命名为(first_layout),根元素就默认选择为 LinearLayout

    1
    2
    3
    4
    5
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    </LinearLayout>
  5. 在onCreate() 方法中加入

    1
    2
    super.onCreate(savedInstanceState); 
    setContentView(R.layout.first_layout);
  6. 修改AndroidManifest.xml文件,将此Activity设为这个程序的主活动

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <activity
    android:name=".FirstActivity"
    android:label="This is FirstActivity"
    android:exported="true">
    <!--android:label=""可以更改当前页面的标题-->
    <!--插入以下内容-->
    <intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
    <!-------------->
    </activity>

日志工具Log

Log.d(tag,msg)

tag:一般传入当前类名 msg:想要打印的具体内容

Toast

onCreate()方法中添加如下代码:

1
2
3
4
var button1 =findViewById<Button>(R.id.button_1)//findViewById<类型>(R.id.ID名)
button1.setOnClickListener{
Toast.makeText(this,"You clicked Button 1",Toast.LENGTH_SHORT).show()
}
  1. res目录下新建一个menu文件夹

  2. 接着在这个文件夹下再新建一个名叫main的菜单文件,右击menu文件夹→NewMenu resource file

  3. 在main.xml中添加如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <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>
  4. 重新回到FirstActivity.kt中,显示出Menu

    1
    2
    3
    4
    5
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {//输入override fun onCreateOptionMenu后点Tab
    menuInflater.inflate(R.menu.main,menu)//插入此行代码
    return super.onCreateOptionsMenu(menu)
    }
    /*menuInflater方法能够得到MenuInflater对象,再调用它的inflate()方法就可以给当前活动创建菜单了。inflate()方法接收两个参数,第一个参数用于指定我们通过哪一个资源文件来创建菜单,这里当然传入R.menu.main。第二个参数用于指定我们的菜单项将添加到哪一个Menu对象当中,这里直接使用onCreateOptionsMenu()方法中传入的menu参数。*/
  5. FirstActivity.kt中,定义菜单响应事件

    1
    2
    3
    4
    5
    6
    7
    override fun onOptionsItemSelected(item: MenuItem): Boolean {//输入override fun onOptionsItemSelected后点Tab
    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 super.onOptionsItemSelected(item)
    }//此方法与4.方法同放在类FirstActivity中

销毁一个活动

finsh() 效果和按下Back键是一样的

使用Intent在活动之间穿梭

Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent一般可被用于启动活动、启动服务以及发送广播等场景。

创建SecondActivity:

  1. 生成SecondActivity.ktsecond_layout.xml(勾选Generate Layout File)

  2. second_layout.xml改成和first_layout.xml一样,但button_1改为button_2

显性Intent

Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class cls) 。这个构造函数接收两个参数,第一个参数Context 要求提供一个启动活动的上下文,第二个参数Class则是指定想要启动的目标活动,通过这个构造函数就可以构建出Intent的“意图”。

修改FirstActivity的按钮的点击事件:

1
2
3
4
5
6
7
var button1 =findViewById<Button>(R.id.button_1)
button1.setOnClickListener(View.OnClickListener {
var intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
/*在FirstActivity这个活动的基础上打开SecondActivity这个活动。然后通过startActivity()方法来执行这个Intent。
按下Back键就可以销毁当前活动,从而回到上一个活动*/
})

隐式Intent

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

  1. 通过在标签下配置的内容,可以指定当前活动能够响应的action和category,打开AndroidManifest.xml,添加如下代码:
1
2
3
4
5
6
<activity android:name=".SecondActivity">
<intent-filter>
<action android:name="com.trrrrw.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>

<activity>标签中我们指明了当前活动可以响应 com.example.activitytest.ACTION_START这个action,而<category>标签则包含了一些附加信息,更精确地指明了当前的活动能够响应的Intent中还可能带有的category。只有<activity><category>中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能响应该Intent。

  1. 修改FirstActivity中按钮的点击事件:

    1
    2
    var intent = Intent("com.trrrrw.activitytest.ACTION_START")
    startActivity(intent)

    android.intent.category.DEFAULT 是一种默认的category,在调用startActivity()方法的时候会自动将这个category添加到Intent中。

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

  1. 修改FirstActivity中按钮的点击事件:

    1
    2
    3
    var intent = Intent("com.trrrrw.activitytest.ACTION_START")
    intent.addCategory("com.trrrrw.activitytest.MY_CATEGORY");
    startActivity(intent)

    此时点击Button 1程序会崩溃,因为SecondActivity的<intent-filter>标签中并没有声明可以响应这个category 。

    在SecondActivity的<intent-filter>标签中加入<category android:name="com.trrrrw.activitytest.MY_CATEGORY"/>即可

更多隐式Intent用法

启动其他程序的Intent
  • 浏览器打开网页:
1
2
3
4
5
button_5.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://www.bilibili.com")
startActivity(intent)
}
  • <intent-filter>标签中配置<data>标签,用于精确指定当前Activity能够相应的数据。

    1. android:scheme 用于指定数据的协议部分;http

    2. android:host 用于指定数据的主机名部分;www.bilibili.com

    3. android:port 用于指定数据的端口部分;一般紧随主机名后

    4. android:mimeType 用于指定可以处理的数据类型;允许使用通配符进行指定

      只有当<data>标签中指定内容与Intent携带的Data完全一致时再能相应

      新建Activity相应网页:

      1. 新建ThirdActivity:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="vertical"
      android:layout_width="match_parent"
      android:layout_height="match_parent">
      <Button android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/second_button_1"
      android:id="@+id/second_button_1"/>
      </LinearLayout>
      1. 更改AndroidMainfest中代码:
      1
      2
      3
      4
      5
      6
      7
      <activity android:name=".ThirdActivity" android:exported="true">
      <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>

      再点击MainActivity中打开网页就可以选择Demo打开

向下一个活动传递数据

在MainActivity中将想传递的数据暂存在Intent中:

1
2
3
4
5
6
7
8
val button_6 = findViewById<Button>(R.id.button_6)
button_6.setOnClickListener {
var data = findViewById<EditText>(R.id.edittext_1)
var sdata = data.text.toString()//读取EditText中的文本并转换成String类型
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("extra_data", sdata)//(键[用于在SecondActivity中将传递的数据取出], 值[传递的数据])
startActivity(intent)
}

在SecondActivity中将传递的数据取出:

1
2
3
val extraData = intent.getStringExtra("extra_data")
val second_textview_1 = findViewById<TextView>(R.id.second_textview_1)
second_textview_1.setText("extra data is $extraData")

返回数据给上一个活动

  • startActivityForResult()方法能够在Activity销毁的时候返回一个结果给上一个Activity。
  1. 在FirstActivity中添加按钮button_7打开SecondActivity

    1
    2
    3
    4
    5
    val button_7 = findViewById<Button>(R.id.button_7)
    button_7.setOnClickListener{
    val intent = Intent(this, SecondActivity::class.java)
    startActivityForResult(intent, 1)//(Intent, 请求码[唯一值即可])
    }
  2. 在SecondActivity中添加按钮点击后返回EditText中的文本并关闭该Activity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    val second_button_2 = findViewById<Button>(R.id.second_button_2)
    second_button_2.setOnClickListener {
    var data = findViewById<EditText>(R.id.second_edittext_1)
    var sdata = data.text.toString()
    val intent = Intent()
    intent.putExtra("data_return", sdata)
    setResult(RESULT_OK, intent)
    finish()
    }//或者将此方法写在onBackPressed()中,用户点击返回按钮后会返回`EditText`中的文本
  3. 使用startActivityForResult()方法打开SecondActivity,在SecondActivity销毁后会回调上一个Activity的onActivityResult()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    when (requestCode) {
    1 -> if (resultCode == RESULT_OK) {
    val returnedData = data?.getStringExtra("data_return")
    val button_7 = findViewById<Button>(R.id.button_7)
    button_7.setText("returned data is $returnedData")
    //将之前打开SecondActivity的按钮上文字改为返回的值
    }
    }
    }//该方法带有三个参数:requestCode为启动Activity时传入的请求码;resultCode为返回数据时传入的结果;data为携带返回数据的Intent
    //通过检测requestCode来判断数据来源

活动的生命周期

返回栈

  • 任务:一组放在栈里的活动的集合

  • 启动一个新的活动,它会在返回栈中入栈,并处于栈顶的位置。

  • Back键或finish()会销毁栈顶的活动

Activity状态

运行状态:位于栈顶,系统最不愿回收

暂停状态:不处于栈顶,但仍可见,系统也不会回收

停止状态:不处于栈顶也不可见,当其他地方需要内存时,可能会被系统回收

销毁状态:从返回栈中移除后的状态,系统倾向于回收此状态的活动

Activity的生存期

  • onCreate()——在活动第一次被创建时调用
  • onStart()——在活动由不可见变为可见的时候调用
  • onResume()——在活动准备好与用户进行交互的时候调用,此时活动位于栈顶,并处于运行状态
  • onPause()——在系统准备去启动或者回复另一个活动的时候调用,通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。
  • onStop()——在活动完全不可见的时候调用,它和 onPause()方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause()方法会得到执行,而onStop()方法并不会执行。
  • onDestory——在活动被销毁之前调用,之后活动变为销毁状态
  • onRestart——在活动由停止状态变为运行状态之前调用,活动被重新启动

生存期

  • 完整生存期:onCreate()与onDestory()之间。一般情况下,一个活动会在onCreate()方法中完成各种初始化操作,而在onDestory()方法中完成释放内存的操作。
  • 可见生存期:onStart()与onStop()之间。期间活动对用户总是可见的,即使可能无法交互。可用于合理管理用户可见的资源,如在onStart()中对资源进行加载,在onStop()中对资源进行释放从而保证处于停止状态的活动不会占用过多内存。
  • 前台生存期:onResume()与onPause()之间。期间活动总是处于运行状态,且可与用户进行交互。

onSaveInstanceState() 回调方法

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

  • 在MainActivity中添加如下代码就可以将临时数据进行保存:
1
2
3
4
5
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
var tempData = "Something you just typed"
outState.putString("data_key",tempdata)
}
  • 修改onCreate()方法:
1
2
3
if (savedInstanceState!=null){
var tempData = savedInstanceState.getString("data_key")
}

Activity的启动模式

standard

活动的默认启动模式,每次启动系统都会为活动创建新的实例放在栈顶。

singleTop

以该模式启动,如果返回栈栈顶是该活动,则会直接使用,不会创建新的活动实例;若不是,则创建新的活动实例。

singleTask

以该模式启动,如果该活动不在栈顶,则将其上方所有活动全部出栈;如果没有,则创建新的活动实例。

singleInstance

以该模式启动,会为该活动创建一个新的返回栈。按Back键会回到该栈中的上一个活动,当该栈中活动清空时,会显示另一个栈。

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

活动的最佳实践

知晓当前是在哪一个Activity

  • 新建一个BaseActivity类:

    • 右键com.trrrrw.demo-新建-Kotlin 类/文件-输入BaseActivity
  • 在BaseActivity中:

    1
    2
    3
    4
    5
    6
    7
    8
    open class BaseActivity : AppCompatActivity(){
    //BaseActivity继承了AppCompatActivity所以在别的Activity继承后功能不会变
    override fun onCreate(savedInstanceState:Bundle?){
    super.onCreate(savedInstanceState)
    Log.d("BaseActivity",javaClass.simpleName)
    //在别的Activity启动时在Logcat中打印当前Activity名
    }
    }
  • 让其余的Activity继承BaseActivity

随时随地退出程序

(+_+)?

Binding视图绑定

MainActivity.kt :

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

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.trrrrw.OF.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
binding = ActivityMainBinding.inflate(layoutInflater)
super.onCreate(savedInstanceState)
setContentView(binding.root)
}
}

build.gradle(:app)(在android下插入) :

1
2
3
buildFeatures{
viewBinding = true
}

Material3

存储库build.gradle文件中要有:

1
2
3
4
5
6
allprojects {
repositories {
google()
mavenCentral()
}
}

应用build.gradle 文件中添加所需工件的依赖项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dependencies {
implementation 'com.google.android.material:material:1.6.0'
}

android {
buildFeatures {
compose = true
}

composeOptions {
kotlinCompilerExtensionVersion = "1.0.0-alpha02"
}

kotlinOptions {
jvmTarget = "1.8"
}
}

修改themes.xml中parent为Theme.Material3.Light

1
<style name="Theme.demo" parent="Theme.Material3.Light">

WebView

1
2
3
4
val webView=binding.webView
webView.settings.javaScriptEnabled=true
webView.webViewClient = WebViewClient()
webView.loadUrl("https://www.epicgames.com/store/zh-CN/free-games")