探究Activity

Activity 的基本用法

创建

新建Activity 对话框

  • Generate Layout File表示会自动为FirstActivity创建一个对应的布局文件;
  • Launcher Activity表示会自动将Activity设置为当前项目的主Activity;
  • 勾选Backwards Compatibility表示会为项目启用向下兼容旧版系统的模式,一般勾选。

创建和加载布局

  • 如果你需要在XML中引用一个id,就使用 @id/id_name这种语法,而如果你需要在XML中定义一个id,则要使用 @+id/id_name这种语法;

  • match_parent表示让当前元素和父元素一样宽;

  • wrap_content表示当前元素的高度只要能刚好包含里面的内容;

  • setContentView() 方法来给当前的Activity加载一个布局,一般传入一个布局文件的id,项目中添加的任何资源都会在R文件中生成一个相应的资源id,因此我们刚才创建的xml布局的id现在已经添加到R文件中了,引用即可;

  • 所有的Activity都要在AndroidManifest.xml中进行注册才能生效,Activity的注册声明要放在<application>标签内,这里是通过标签<application>来对Activity进行注册的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    package="com.example.activitytest">
    <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".FirstActivity">
    </activity>
    </application>
    </manifest>

    在<activity>标签中,我们使用了android:name来指定具体注册哪一个Activity,那么这里填入的.FirstActivity是com.example.activitytest.FirstActivity的缩写而已。由于在最外层的标签中已经通过package属性指定了程序的包名是com.example.activitytest,因此在注册Activity时,这一部分可以省略;

  • 配置主Activity在<activity>标签的内部加入<intent-filter>标签,并在这个标签里添加两句声明即可

    1
    2
    3
    4
    <intent-filter> 
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

使用Toast

Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间。

1
2
3
4
5
6
7
8
override fun onCreate(savedInstanceState: Bundle?) { 
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout)
val button1: Button = findViewById(R.id.button1)
button1.setOnClickListener {
Toast.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT).show()
}
}

通过findViewById() 方法获取在布局文件中定义的元素,这里我们传入id来得到按钮的实例。findViewById()方法返回的是一个继承自View的泛型对象,因此Kotlin无法自动推导出它是一个Button还是其他控件,所以我们需要显式地声明成类型。

注意,原书中使用kotlin-android-extensions插件自动获取一个具有相同名称的变量的办法已经行不通,谷歌官方已经舍弃该插件,使用其他方法进行替代。

使用Menu

  • 老版本IDE可以尝试按照原书中的教程,新版IDE应该直接创建Layout xml file,然后根改为menu。

  • 重写onCreateOptionsMenu()方法:

    1
    2
    3
    4
    override fun onCreateOptionsMenu(menu: Menu?): Boolean { 
    menuInflater.inflate(R.menu.main, menu)
    return true
    }

    这里是Java Bean的概念,它是一个非常简单的Java类,会根据类中的字段自动生成相应的Getter和Setter方法。在Kotlin中调用这种语法结构的Java方法时,可直接对字段进行了赋值和读取。其实这就是Kotlin给我们提供的语法糖,它会在背后自动将上述代码转换成调用setPages()方法和getPages()方法。

    menuInflater就使用了这种语法糖,它实际上是调用了父类的getMenuInflater() 方法。getMenuInflater()方法能够得到一个MenuInflater对象,再调用它的inflate()方法,就可以给当前Activity创建菜单了。inflate() 方法接收两个参数:第一个参数用于指定我们通过哪一个资源文件来创建菜单,这里当然是传入R.menu.main;第二个参数用于指定我们的菜单项将添加到哪一个Menu对象当中,这里直接使用onCreateOptionsMenu()方法中传入的menu参数。最后给这个方法返回true,表示允许创建的菜单显示出来,如果返回了false,创建的菜单将无法显示。

    注意:menuInflater是Context的一个成员变量,getMenuInflater()是Context的一个方法,Activity继承自Context。

销毁

Activity类提供了一个finish() 方法,我们只需要调用一下这个方法就可以销毁当前的Activity。

1
2
3
button1.setOnClickListener { 
finish()
}

使用Intent

Intent用于跳转不同的Activity。

使用显式Intent

Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class cls) 。这个构造函数接收两个参数:第一个参数Context要求提供一个启动Activity的上下文;第二个参数Class用于指定想要启动的目标Activity,通过这个构造函数就可以构建出Intent的“意图”。Activity类中提供了一个startActivity()方法,专门用于启动Activity,它接收一个Intent参数,这里我们将构建好的Intent传入startActivity()方法就可以启动目标Activity了。

1
2
3
4
button1.setOnClickListener { 
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}

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

使用隐式Intent

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

通过在<activity>标签下配置<intent-filter>的内容,可以指定当前Activity能够响应的action和category,在AndroidManifest.xml中进行配置:

1
2
3
4
5
6
<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。

  1. 最新版中,应将组件导出项android:exported设置为true,否则隐式调用会报错;
  2. <category>中必须有DEFAULT,因为无论啥,底层会自动添加DEFAULT策略,没有会导致程序崩溃。

更多隐式Intent 的用法

  1. 网页

    1
    2
    3
    4
    5
    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对象传递进去。

  2. 拨号

    暂不赘述。

还有很多用法不一一列举。

向下传递数据

Intent中提供了一系列putExtra()方法的重载,可以把我们想要传递的数据暂存在Intent中,在启动另一个Activity后,只需要把这些数据从Intent中取出就可以了。

1
2
3
4
5
6
button1.setOnClickListener { 
val data = "Hello SecondActivity"
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("extra_data", data)
startActivity(intent)
}

调用的父类的getIntent() 方法,该方法会获取用于启动SecondActivity的Intent,然后调用getStringExtra()方法并传入相应的键值,就可以得到传递的数据了

1
2
3
4
5
6
7
8
class SecondActivity : AppCompatActivity() { 
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.second_layout)
val extraData = intent.getStringExtra("extra_data")
Log.d("SecondActivity", "extra data is $extraData")
}
}

向上返回数据

Activity类中还有一个用于启动Activity的startActivityForResult() 方法,但它期望在Activity销毁的时候能够返回一个结果给上一个Activity。

startActivityForResult()方法接收两个参数:第一个参数还是Intent;第二个参数是请求码,用于在之后的回调中判断数据的来源。

1
2
3
4
button1.setOnClickListener { 
val intent = Intent(this, SecondActivity::class.java)
startActivityForResult(intent, 1)
}

setResult() 方法接收两个参数:第一个参数用于向上一个Activity返回处理结果,一般只使用RESULT_OK或RESULT_CANCELED这两个值;第二个参数则把带有数据的Intent传递回去。最后调用了finish()方法来销毁当前Activity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SecondActivity : AppCompatActivity() { 

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.second_layout)
button2.setOnClickListener {
val intent = Intent()
intent.putExtra("data_return", "Hello FirstActivity")
setResult(RESULT_OK, intent)
finish()
}
}

}

由于我们是使用startActivityForResult()方法来启动SecondActivity的,在SecondActivity被销毁之后会回调上一个Activity的onActivityResult() 方法,因此我们需要重写这个方法来得到返回的数据。

onActivityResult()方法带有3个参数:第一个参数requestCode,即我们在启动Activity时传入的请求码;第二个参数resultCode,即我们在返回数据时传入的处理结果;第三个参数data,即携带着返回数据的Intent。

1
2
3
4
5
6
7
8
9
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")
Log.d("FirstActivity", "returned data is $returnedData")
}
}
}

如果用户在SecondActivity中并不是通过点击按钮,而是通过按下Back键回到FirstActivity,可以通过在SecondActivity中重写onBackPressed() 方法来解决这个问题。

生命周期

Android是使用任务(task)来管理Activity的,而每当我们按下Back键或调用finish()方法去销毁一个Activity时,处于栈顶的Activity就会出栈。系统总是会显示处于栈顶的Activity给用户。

本小节了解完整生存期、可见生存期、前台生存期概念即可。

QQ_1736482183719

  • onCreate() 它会在Activity第一次被创建的时候调用。你应该在这个方法中完成Activity的初始化操作,比如加载布局、绑定事件等;
  • onStart() 这个方法在Activity由不可见变为可见的时候调用;
  • onResume() 这个方法在Activity准备好和用户进行交互的时候调用;
  • onPause() 这个方法在系统准备去启动或者恢复另一个Activity的时候调用;
  • onStop() 这个方法在Activity完全不可见的时候调用;
  • onDestroy() 这个方法在Activity被销毁之前调用,之后Activity的状态将变为销毁状态;
  • onRestart() 这个方法在Activity由停止状态变为运行状态之前调用,也就是Activity被重新启动了。

小知识:android:theme属性,用于给当前Activity指定主题,@style/Theme.AppCompat.Dialog则毫无疑问是使用对话框式的主题。

回收时数据保存

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

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

最后,保存的Bundle类型的参数将传入onCreate()方法。

1
2
3
4
5
6
7
8
9
10
override fun onCreate(savedInstanceState: Bundle?) { 
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
setContentView(R.layout.activity_main)
if (savedInstanceState != null) {
val tempData = savedInstanceState.getString("data_key")
Log.d(tag, "tempData is $tempData")
}
...
}

当手机的屏幕发生旋转的时候,Activity也会经历一个重新创建的过程,因而在这种情况下,Activity中的数据也会丢失。虽然这个问题同样可以通过onSaveInstanceState()方法来解决,但是一般不太建议这么做,因为对于横竖屏旋转的情况,现在有更加优雅的解决方案。

启动模式

启动模式一共有4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给<activity>标签指定android:launchMode属性来选择启动模式。

  1. standard(默认):不考虑Activity重复,每次创建新的Activity;

  2. singleTop:只考虑栈顶Activity不重复;

  3. singleTask:考虑Activity的重复问题,但是回退到Activity时前面的所有Activity将出栈丢失;

  4. singleInstance:使用单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都共用同一个返回栈,解决共享Activity实例的问题。

    QQ_1736515722571

    SecondActivity不同于FirstActivity和ThirdActivity,SecondActivity存放在一个单独的返回栈里,而且这个栈中只有SecondActivity这一个Activity。按下Back键进行返回,ThirdActivity返回到了FirstActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序。

实践

知晓当前Activity

创建BaseActivity,让其他Activity继承,同时在BaseActivity内部打印当前活动的Activity即可。

1
2
3
4
5
6
open class BaseActivity : AppCompatActivity() { 
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("BaseActivity", javaClass.simpleName)
}
}

Kotlin中的javaClass表示获取当前实例的Class对象,相当于在Java中调用getClass()方法;而Kotlin中的BaseActivity::class.java表示获取BaseActivity类的Class对象,相当于在Java中调用BaseActivity.class。注意,后者显式Intent切换Activity会用到。

思路点拨:子类重写onCreate方法时会覆盖父类方法,但是为什么这样可行呢?因为子类的onCreate方法第一句按照规定应为调用父类的onCreate方法,因此生效。

退出Activity

使用一次Back按键达到退出目的,思路是使用一个专门的类对所有的Activity进行管理。

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
object ActivityCollector { 
private val activities = ArrayList<Activity>()
fun addActivity(activity: Activity) {
activities.add(activity)
}
fun removeActivity(activity: Activity) {
activities.remove(activity)
}
fun finishAll() {
for (activity in activities) {
if (!activity.isFinishing) {
activity.finish()
}
}
activities.clear()
}
}
open class BaseActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("BaseActivity", javaClass.simpleName)
ActivityCollector.addActivity(this)
}

override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivity(this)
}
}

class ThirdActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("ThirdActivity", "Task id is $taskId")
setContentView(R.layout.third_layout)
button3.setOnClickListener {
ActivityCollector.finishAll()
}
}
}

当然你还可以在销毁所有Activity的代码后面再加上杀掉当前进程的代码,以保证程序完全退出:

1
android.os.Process.killProcess(android.os.Process.myPid()) 

killProcess()方法用于杀掉一个进程,它接收一个进程id参数,我们可以通过myPid()方法来获得当前程序的进程id。需要注意的是,killProcess()方法只能用于杀掉当前程序的进程,不能用于杀掉其他程序。

启动规范

简化启动流程,并且接口规范化,利于团队化编程。养成一个良好的习惯,给你编写的每个Activity都添加类似的启动方法,这样不仅可以让启动Activity变得非常简单,还可以节省不少你同事过来询问你的时间。

1
2
3
4
5
6
7
8
9
10
11
class SecondActivity : BaseActivity() { 
...
companion object {
fun actionStart(context: Context, data1: String, data2: String) {
val intent = Intent(context, SecondActivity::class.java)
intent.putExtra("param1", data1)
intent.putExtra("param2", data2)
context.startActivity(intent)
}
}
}

标准函数和静态方法

标准函数with、run和apply

前情提要:let标准函数,主要作用是配合?.操作符来进行辅助判空处理。

with

用于在连续调用同一个对象的多个方法时,让代码变得更加精简。

with函数接收两个参数:第一个参数可以是一个任意类型的对象,第二个参数是一个Lambda表达式。with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val result = with(obj) { 
// 这里是obj的上下文
"value" // with函数的返回值
}

// 吃完所有水果,并将结果打印出来
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = with(StringBuilder()) {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
toString()
}
println(result)

run

run函数的用法和使用场景其实和with函数是非常类似的,只是稍微做了一些语法改动而已。

首先run函数通常不会直接调用,而是要在某个对象的基础上调用;其次run函数只接收一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文,使用Lambda表达式中的最后一行代码作为返回值返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val result = obj.run { 
// 这里是obj的上下文
"value" // run函数的返回值
}

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().run {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
toString()
}
println(result)

apply

apply函数和run函数也是极其类似的,都要在某个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val result = obj.apply { 
// 这里是obj的上下文
}
// result == obj


val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().apply {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())

由于apply函数无法指定返回值,只能返回调用对象本身,因此这里的result实际上是一个StringBuilder对象,所以我们在最后打印的时候还要再调用它的toString()方法才行。

定义静态方法

类静态方法

Kotlin极度弱化了静态方法这个概念,因为Kotlin提供了比静态方法更好用的语法特性,那就是单例类。

不过,使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,而如果我们只是希望让类中的某一个方法变成静态方法的调用方式,可以使用companion object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Util { 

fun doAction1() {
println("do action1")
}

companion object {

fun doAction2() {
println("do action2")
}

}

}

doAction2()方法其实也并不是静态方法,实质在Util类的内部创建一个伴生类,因此调用Util.doAction2()方法实际上就是调用了Util类中伴生对象的doAction2()方法。

静态方法

解决在Java代码中以静态方法的形式去调用companion object和单例类会报错,因为这些不是真正的静态方法。

要定义真正的静态方法,Kotlin仍然提供了两种实现方式:注解和顶层方法

而如果我们给单例类或companion object中的方法加上 @JvmStatic注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法。

注意,@JvmStatic注解只能加在单例类或companion object中的方法上,如果你尝试加在一个普通方法上,会直接提示语法错误。

顶层方法指的是那些没有定义在任何类中的方法。Kotlin编译器会将所有的顶层方法全部编译成静态方法。

在Java代码中调用,因为Java中没有顶层方法这个概念,且所有的方法必须定义在类中。Kotlin编译器会自动创建一个叫作HelperKt的Java类,doSomething()方法就是以静态方法的形式定义在HelperKt类里面的,因此在Java中使用HelperKt.doSomething()的写法来调用就可以了。