Kotlin安卓开发入门
Kotlin安卓开发入门
Rookie_l一、ViewBinding
1.1 什么是ViewBinding
ViewBinding总体来说其实非常简单,它的目的只有一个,就是为了避免编写findViewById
,这和它另外一个非常复杂的兄弟DataBinding相比有明显的区别。
要想使用ViewBinding需要注意两件事。第一,确保你的Android Studio是3.6或更高的版本。第二,在你项目工程模块的build.gradle
中加入以下配置:
1 | android { |
1.2 在Activity中使用ViewBinding
一旦启动了ViewBinding功能之后,Android Studio会自动为我们所编写的每一个布局文件都生成一个对应的Binding
类.Binding
类的命名规则是将布局文件按驼峰方式重命名后,再加上Binding
作为结尾。比如说,前面我们定义了一个activity_main.xml
布局,那么与它对应的Binding
类就是ActivityMainBinding
。当然,如果有些布局文件你不希望为它生成对应的Binding
类,可以在该布局文件的根元素位置加入如下声明:
1 | <LinearLayout |
接下来我们看一下如何使用ViewBinding来实现在MainActivity中去设置TextView内容的功能,代码如下所示:
1 | class MainActivity : AppCompatActivity() { |
ViewBinding的用法可以说就是这么简单。首先我们要调用activity_main.xml
布局文件对应的Binding
类,也就是ActivityMainBinding
的inflate()
函数去加载该布局,inflate()
函数接收一个LayoutInflater
参数,在Activity
中是可以直接获取到的。
接下来就更加简单了,调用Binding
类的getRoot()
函数可以得到activity_main.xml
中根元素的实例,调用getTextView()
函数可以获得id
为textView
的元素实例。
那么很明显,我们应该把根元素的实例传入到setContentView()
函数当中,这样Activity
就可以成功显示activity_main.xml
这个布局的内容了。然后获取TextView
控件的实例,并给它设置要显示的文字即可。
当然,如果你需要在onCreate()
函数之外的地方对控件进行操作,那么就得将binding
变量声明成全局变量,写法如下:
1 | class MainActivity : AppCompatActivity() { |
Kotlin声明的变量都必须在声明的同时对其进行初始化。而这里我们显然无法在声明全局binding变量的同时对它进行初始化,所以这里又使用了lateinit关键字对binding变量进行了延迟初始化。
二、Intent
Intent大致可以分为两种:显式Intent和隐式Intent。我们先来看一下显式Intent如何使用。Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<?>cls)
。这个构造函数接收两个参数:第一个参数Context
要求提供一个启动Activity
的上下文;第二个参数Class
用于指定想要启动的目标Activity
,通过这个构造函数就可以构建出Intent
的“意图”。那么接下来我们应该怎么使用这个Intent
呢?Activity
类中提供了一个startActivity()
方法,专门用于启动Activity
,它接收一个Intent
参数,这里我们将构建好的Intent传入startActivity()方法就可以启动目标Activity了。
2.1 显式Intent
Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent一般可用于启动Activity、启动Service以及发送广播等场景。
1 | override fun onCreate(savedInstanceState: Bundle?) { |
我们首先构建了一个Intent对象,第一个参数传入this
也就是FirstActivity
作为上下文,第二个参数传入SecondActivity::class.java
作为目标Activity
,这样我们的“意图”就非常明显了,即在FirstActivity
的基础上打开SecondActivity
。注意,Kotlin中SecondActivity::class.java
的写法就相当于Java中SecondActivity.class
的写法。接下来再通过startActivity()
方法执行这个Intent就可以了。
2.2 隐式Intent
相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出想要启动哪一个Activity
,而是指定了一系列更为抽象的action
和category
等信息,然后交由系统去分析这个Intent,并帮我们找出合适的Activity
去启动。
通过在<activity>
标签下配置<intent-filter>
的内容,可以指定当前Activity
能够响应的action
和category
,打开AndroidManifest.xml
,添加如下代码:
1 | <activity |
在<action>
标签中我们指明了当前Activity
可以响应com.example.activitytest.ACTION_START
这个action
,而<category>
标签则包含了一些附加信息,更精确地指明了当前Activity
能够响应的Intent
中还可能带有的category
。只有<action>
和<category>
中的内容同时匹配Intent
中指定的action
和category
时,这个Activity
才能响应该Intent
。
1 | binding.button2.setOnClickListener { |
2.2.1 更多隐式Intent的用法
使用隐式Intent,不仅可以启动自己程序内的Activity
,还可以启动其他程序的Activity
,这就使多个应用程序之间的功能共享成为了可能。比如你的应用程序中需要展示一个网页,这时你没有必要自己去实现一个浏览器(事实上也不太可能),只需要调用系统的浏览器来打开这个网页就行了。
1 | binding.button2.setOnClickListener { |
首先指定了Intent的action
是Intent.ACTION_VIEW
,这是一个Android
系统内置的动作,其常量值为android.intent.action.VIEW
。然后通过Uri.parse()
方法将一个网址字符串解析成一个Uri
对象,再调用Intent的setData()
方法将这个Uri对象传递进去。当然,这里再次使用了前面学习的语法糖,看上去像是给Intent的data
属性赋值一样。
我们还可以在<intent-filter>
标签中再配置一个<data>
标签,用于更精确地指定当前Activity
能够响应的数据。<data>
标签中主要可以配置以下内容。
android:scheme
:用于指定数据的协议部分,如上例中的https部分;android:host
:用于指定数据的主机名部分,如上例中的www.baidu.com部分;android:port
:用于指定数据的端口部分,一般紧随在主机名之后;android:path
:用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容;android:mimeType
:用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
只有当<data>
标签中指定的内容和Intent中携带的Data
完全一致时,当前Activity
才能够响应该Intent。不过,在<data>
标签中一般不会指定过多的内容。例如在上面的浏览器示例中,其实只需要指定android:scheme
为https,就可以响应所有https协议的Intent了。
除了https协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。下面的代码展示了如何在我们的程序中调用系统拨号界面。
1 | binding.button2.setOnClickListener { |
2.3 向下一个Activity传递数据
到目前为止,我们只是简单地使用Intent来启动一个Activity
,其实Intent在启动Activity
的时候还可以传递数据。
在启动Activity
时传递数据的思路很简单,Intent中提供了一系列putExtra()
方法的重载,可以把我们想要传递的数据暂存在Intent中,在启动另一个Activity
后,只需要把这些数据从Intent中取出就可以了。比如说FirstActivity
中有一个字符串,现在想把这个字符串传递到SecondActivity
中,你就可以这样编写:
1 | binding.button1.setOnClickListener{ |
这里我们还是使用显式Intent的方式来启动SecondActivity
,并通过putExtra()
方法传递了一个字符串。注意,这里putExtra()
方法接收两个参数,第一个参数是键,用于之后从Intent中取值,第二个参数才是真正要传递的数据。然后在SecondActivity
中将传递的数据取出,并打印出来,代码如下所示:
1 | class SecondActivity : AppCompatActivity() { |
上述代码中的intent实际上调用的是父类的getIntent()
方法,该方法会获取用于启动SecondActivity
的Intent,然后调用getStringExtra()
方法并传入相应的键值,就可以得到传递的数据了。这里由于我们传递的是字符串,所以使用getStringExtra()
方法来获取传递的数据。如果传递的是整型数据,则使用getIntExtra()
方法;如果传递的是布尔型数据,则使用getBooleanExtra()
方法,以此类推。
2.4 返回数据给上一个Activity
既然可以传递数据给下一个Activity
,那么能不能够返回数据给上一个Activity
呢?答案是肯定的。不过不同的是,返回上一个Activity
只需要按一下Back键就可以了,并没有一个用于启动Activity
的Intent来传递数据,这该怎么办呢?其实Activity
类中还有一个用于启动Activity
的startActivityForResult()
方法,但它期望在Activity
销毁的时候能够返回一个结果给上一个Activity
。毫无疑问,这就是我们所需要的。
startActivityForResult()
方法接收两个参数:第一个参数还是Intent;第二个参数是请求码,用于在之后的回调中判断数据的来源。我们还是来实战一下,修改FirstActivity
中按钮的点击事件,代码如下所示:
1 | companion object { |
这里我们使用了startActivityForResult()
方法来启动SecondActivity
,请求码只要是一个唯一值即可,这里传入了1024
。接下来我们在SecondActivity
中给按钮注册点击事件,并在点击事件中添加返回数据的逻辑,代码如下所示:
1 | bindings.button1.setOnClickListener { |
上述方式在在``androidx.activity-1.2.0-alpha04时开始就已经被弃用了.Android中这位被调用过无数次的
startActivityForResult和
onActivityResult,已经被官方标记为弃用了,继而推出了名为
Activity Result API`的组件。
1 | class FirstActivity : AppCompatActivity() { |
这里其实分为三个部分:对载体、定义协定、回调3个类分别定义写出来。
1 | private lateinit var resultLauncher: ActivityResultLauncher<Intent> |
其实大部分情况下,我们可以这样写:
1 | private val launcherActivity = registerForActivityResult( |
是不是瞬间清爽了许多,但是…你还是觉得比使用startActivityForResult
更复杂?其实不然,因为上面代码的需求是一个单一的回调,所以看着似乎startActivityForResult
更便于维护和使用。但倘若编写一个稍复杂的页面,需要同时请求相册、需要在其它Activity选择数据并回调、需要判断权限等等时,继续使用startActivityForResult
,会导致onActivityResult
里掺杂各种嵌套及判断,导致代码难以维护。而使用registerForActivityResult()
可以多次调用以注册多个 ActivityResultLauncher
实例,用来处理不同的Activity结果,让代码更便于维护。
优势了解到了,但既然需要使用新的功能,那么我们就必须要先了解以下,刚说到的ActivityResultLauncher
、ActivityResultContract
、ActivityResultCallback
到底是些什么东西
- ActivityResultLauncher 从字面意思其实就能很好理解,可以理解它就是一个Activity的启动器,它的作用就是承载启动对象与返回对象,通过
registerForActivityResult
返回该对象,这时并不会立即启动另一个Activity。 - ActivityResultContract 是用来协定所需的输入类型以及结果的输出类型,Android默认提供了一些常用的定义,例如上面所使用到到
ActivityResultContracts.StartActivityForResult()
。当然这里你也可以通过继承ActivityResultContract
实现自己的定义。 - ActivityResultCallback 通过名字就可以了解到这是启动Activity并返回到当前Activity时的结果回调。
对于这3个类,其实只需重点了解ActivityResultContract
,就能很轻松的理解并使用好Activity Result API
了。
官方文档的警告
注意:虽然在 fragment 或 activity 创建完毕之前可安全地调用
registerForActivityResult()
,但在 fragment 或 activity 的Lifecycle
变为CREATED
状态之前,您无法启动ActivityResultLauncher
。
三、Activity的生命周期
掌握Activity的生命周期对任何Android开发者来说都非常重要,当你深入理解Activity的生命周期之后,就可以写出更加连贯流畅的程序,并在如何合理管理应用资源方面发挥得游刃有余。你的应用程序也将会拥有更好的用户体验。
3.1 返回栈
经过前面几节的学习,相信你已经发现了Android中的Activity是可以层叠的。我们每启动一个新的Activity,就会覆盖在原Activity之上,然后点击Back键会销毁最上面的Activity,下面的一个Activity就会重新显示出来。
其实Android是使用任务(task)来管理Activity的,一个任务就是一组存放在栈里的Activity的集合,这个栈也被称作返回栈(back stack)。栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。而每当我们按下Back键或调用finish()
方法去销毁一个Activity时,处于栈顶的Activity就会出栈,前一个入栈的Activity就会重新处于栈顶的位置。系统总是会显示处于栈顶的Activity给用户。
3.2 Activity状态
每个Activity在其生命周期中最多可能会有4种状态。
3.2.1 运行状态
当一个Activity位于返回栈的栈顶时,Activity就处于运行状态。系统最不愿意回收的就是处于运行状态的Activity,因为这会带来非常差的用户体验。
3.2.2 暂停状态
当一个Activity不再处于栈顶位置,但仍然可见时,Activity就进入了暂停状态。你可能会觉得,既然Activity已经不在栈顶了,怎么会可见呢?这是因为并不是每一个Activity都会占满整个屏幕,比如对话框形式的Activity只会占用屏幕中间的部分区域。处于暂停状态的Activity仍然是完全存活着的,系统也不愿意回收这种Activity(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种Activity。
3.2.3 停止状态
当一个Activity不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种Activity保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的Activity有可能会被系统回收。
3.2.4 销毁状态
一个Activity从返回栈中移除后就变成了销毁状态。系统最倾向于回收处于这种状态的Activity,以保证手机的内存充足。
3.3 Activity的生存期
Activity类中定义了7个回调方法,覆盖了Activity生命周期的每一个环节,下面就来一一介绍这7个方法。
onCreate()
:这个方法你已经看到过很多次了,我们在每个Activity中都重写了这个方法,它会在Activity第一次被创建的时候调用。你应该在这个方法中完成Activity的初始化操作,比如加载布局、绑定事件等。onStart()
:这个方法在Activity由不可见变为可见的时候调用。onResume()
:这个方法在Activity准备好和用户进行交互的时候调用。此时的Activity一定位于返回栈的栈顶,并且处于运行状态。onPause()
:这个方法在系统准备去启动或者恢复另一个Activity的时候调用。我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶Activity的使用。onStop()
:这个方法在Activity完全不可见的时候调用。它和onPause()方法的主要区别在于,如果启动的新Activity是一个对话框式的Activity,那么onPause()方法会得到执行,而onStop()方法并不会执行。onDestroy()
:这个方法在Activity被销毁之前调用,之后Activity的状态将变为销毁状态。onRestart()
:这个方法在Activity由停止状态变为运行状态之前调用,也就是Activity被重新启动了。
以上7个方法中除了onRestart()
方法,其他都是两两相对的,从而又可以将Activity分为以下3种生存期。
- 完整生存期:Activity在
onCreate()
方法和onDestroy()
方法之间所经历的就是完整生存期。一般情况下,一个Activity会在onCreate()方法中完成各种初始化操作,而在onDestroy()方法中完成释放内存的操作。 - 可见生存期:Activity在
onStart()
方法和onStop()
方法之间所经历的就是可见生存期。在可见生存期内,Activity对于用户总是可见的,即便有可能无法和用户进行交互。我们可以通过这两个方法合理地管理那些对用户可见的资源。比如在onStart()
方法中对资源进行加载,而在onStop()
方法中对资源进行释放,从而保证处于停止状态的Activity不会占用过多内存。 - 前台生存期:Activity在
onResume()
方法和onPause()
方法之间所经历的就是前台生存期。在前台生存期内,Activity总是处于运行状态,此时的Activity是可以和用户进行交互的,我们平时看到和接触最多的就是这个状态下的Activity。
为了帮助我们理解,Android官方提供了一张Activity生命周期的示意图,如图所示:
3.4 体验Activity的生命周期
如果我们重写Activity中的:onCreate()
、onStart()
、onResume()
、onPause()
、onStop()
、onDestroy()
和onRestart()
方法并打印log,我们就可以发现:Activity在第一次被创建时会依次执行onCreate()
、onStart()
和onResume()方法。如下所示:
Activity启动后 我们按下Home键:由于MainAtivity已经不再可见,因此onPause()
和onStop()
方法都会得到执行。
接下来我们再次进入应用,由于之前MainActivity已经进入了停止状态,所以onRestart()
方法会得到执行,之后会依次执行onStart()
和onResume()
方法。注意,此时onCreate()
方法不会执行,因为MainActivity并没有重新创建。
假如我们在MainActivity当中启动了一个Dialog,此时我们再去观察打印信息就会发现:
只有onPause()
方法得到了执行,onStop()
方法并没有执行,这是因为DialogActivity并没有完全遮挡住MainActivity,此时MainActivity只是进入了暂停状态,并没有进入停止状态。相应地,按下Back键返回MainActivity也应该只有onResume()
方法会得到执行
当调用了 finish()
方法结束Activity时,依次会执行onPause()
、onStop()
和onDestroy()
方法,最终销毁MainActivity。
3.5 Activity被回收了怎么办
前面我们说过,当一个Activity进入了停止状态,是有可能被系统回收的。那么想象以下场景:应用中有一个Activity A,用户在Activity A的基础上启动了Activity B,Activity A就进入了停止状态,这个时候由于系统内存不足,将Activity A回收掉了,然后用户按下Back键返回Activity A,会出现什么情况呢?其实还是会正常显示Activity A的,只不过这时并不会执行onRestart()方法,而是会执行Activity A的onCreate()方法,因为Activity A在这种情况下会被重新创建一次。
这样看上去好像一切正常,可是别忽略了一个重要问题:Activity A中是可能存在临时数据和状态的。打个比方,MainActivity中如果有一个文本输入框,现在你输入了一段文字,然后启动NormalActivity,这时MainActivity由于系统内存不足被回收掉,过了一会你又点击了Back键回到MainActivity,你会发现刚刚输入的文字都没了,因为MainActivity被重新创建了。
如果我们的应用出现了这种情况,是会比较影响用户体验的,所以得想想办法解决这个问题。其实,Activity中还提供了一个onSaveInstanceState()
回调方法,这个方法可以保证在Activity被回收之前一定会被调用,因此我们可以通过这个方法来解决问题。
onSaveInstanceState()
方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的内容。
在MainActivity当中添加如下代码就可以将临时数据进行保存了:
1 | override fun onSaveInstanceState(outState: Bundle) { |
数据是已经保存下来了,那么我们应该在哪里进行恢复呢?细心的你也许早就发现,我们一直使用的onCreate()方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是null,但是如果在Activity被系统回收之前,你通过onSaveInstanceState()方法保存数据,这个参数就会带有之前保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可。
修改MainActivity方法:
1 | override fun onCreate(savedInstanceState: Bundle?) { |
取出值之后再做相应的恢复操作就可以了,比如将文本内容重新赋值到文本输入框上.
不知道你有没有察觉,使用Bundle保存和取出数据是不是有些似曾相识呢?没错!我们在使用Intent传递数据时也用的类似的方法。这里提醒一点,Intent还可以结合Bundle一起用于传递数据。首先我们可以把需要传递的数据都保存在Bundle对象中,然后再将Bundle对象存放在Intent里。到了目标Activity之后,先从Intent中取出Bundle,再从Bundle中一一取出数据。
四、Activity的启动模式
在实际项目中我们应该根据特定的需求为每个Activity指定恰当的启动模式。启动模式一共有4种,分别是standard
、singleTop
、singleTask
和singleInstance
,可以在AndroidManifest.xml中通过给**<activity>
**标签指定android:launchMode
属性来选择启动模式。
4.1 standard
standard是Activity默认的启动模式,在不进行显式指定的情况下,所有Activity都会自动使用这种启动模式。Android是使用返回栈来管理Activity的,在standard模式下,每当启动一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的Activity,系统不会在乎这个Activity是否已经在返回栈中存在,每次启动都会创建一个该Activity的新实例。
standard模式的原理:
4.2 singleTop
可能在有些情况下,standard模式不太合理。Activity明明已经在栈顶了,为什么再次启动的时候还要创建一个新的Activity实例呢?
别着急,这只是系统默认的一种启动模式而已,你完全可以根据自己的需要进行修改,比如使用singleTop模式。当Activity的启动模式指定为singleTop,在启动Activity时如果发现返回栈的栈顶已经是该Activity,则认为可以直接使用它,不会再创建新的Activity实例。
在 AndroidManifest.xml中可以如下设置:android:launchMode="singleTop"
1 | <activity |
有这么一种情况:从 FirstActivity启动 SecondActivity 再从 SecondActivity启动 FirstActivity。
此时系统就创建了两个不同的FirstActivity实例,这是由于在SecondActivity中再次启动FirstActivity时,栈顶Activity已经变成了SecondActivity,因此会创建一个新的FirstActivity实例。
4.3 singleTask
使用singleTop模式可以很好地解决重复创建栈顶Activity的问题,如果该Activity并没有处于栈顶的位置,还是可能会创建多个Activity实例的。那么有没有什么办法可以让某个Activity在整个应用程序的上下文中只存在一个实例呢?这就要借助singleTask模式来实现了。当Activity的启动模式指定为singleTask,每次启动该Activity时,系统首先会在返回栈中检查是否存在该Activity的实例,如果发现已经存在则直接使用该实例,并把在这个Activity之上的所有其他Activity统统出栈,如果没有发现就会创建一个新的Activity实例。
通过代码更直观的感受一下
首先从 FirstActivity启动 SecondActivity 再从 SecondActivity启动 FirstActivity。在相应的生存周期加上日志打印:
其实从打印信息中就可以明显看出,在SecondActivity中启动FirstActivity时,会发现返回栈中已经存在一个FirstActivity的实例,并且是在SecondActivity的下面,于是SecondActivity会从返回栈中出栈,而FirstActivity重新成为了栈顶Activity,因此FirstActivity的onRestart()
方法和SecondActivity的onDestroy()
方法会得到执行。现在返回栈中只剩下一个FirstActivity的实例了,按一下Back键就可以退出程序。
SingleTask 模式原理
4.3 singleInstance
singleInstance模式应该算是4种启动模式中最特殊也最复杂的一个了。不同于以上3种启动模式,指定为singleInstance模式的Activity会启用一个新的返回栈来管理这个Activity(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈)。那么这样做有什么意义呢?想象以下场景,假设我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,应该如何实现呢?使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。而使用singleInstance模式就可以解决这个问题,在这种模式下,会有一个单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都共用同一个返回栈,也就解决了共享Activity实例的问题。
为了更好理解这种启动模式,接下来实践一下:在AndroidManifest.xml
中修改启动模式为 singleInstance
1 | <activity |
然后从 MainActivity启动 NormalActivity,再从NormalActivity启动ThirdActivity。并在这几个Activity中分别打印出TaskID。
通过打印我们可以发现 MainActivity是单独在一个返回栈的,NormalActivity和ThirdActivity在另外一个返回栈。
singleInstance模式的原理如图所示
五、Activity的最佳实践
5.1 知晓当前是在哪一个Activity
我们还是在ActivityTest项目的基础上修改,首先需要新建一个BaseActivity类。右击com.example.activitytest包→New→Kotlin File/Class,在弹出的窗口中输入BaseActivity,创建类型选择Class。让BaseActivity继承自AppCompatActivity,并重写onCreate()方法,如下所示:
1 | open class BaseActivity : AppCompatActivity() { |
我们在onCreate()方法中加了一行日志,用于打印当前实例的类名。这里我要额外说明一下,Kotlin中的javaClass表示获取当前实例的Class对象,相当于在Java中调用getClass()方法;而Kotlin中的BaseActivity::class.java表示获取BaseActivity类的Class对象,相当于在Java中调用BaseActivity.class。在上述代码中,我们先是获取了当前实例的Class对象,然后再调用simpleName获取当前实例的类名。
下来我们需要让BaseActivity成为ActivityTest项目中所有Activity的父类,为了使BaseActivity可以被继承,我已经提前在类名的前面加上了open关键字。然后修改FirstActivity、SecondActivity和ThirdActivity的继承结构,让它们不再继承自AppCompatActivity,而是继承自BaseActivity。而由于BaseActivity又是继承自AppCompatActivity的,所以项目中所有Activity的现有功能并不受影响,它们仍然继承了Activity中的所有特性。
通过日志可以看到,启动的Activity都被打印出来了。现在每当我们进入一个Activity的界面,该Activity的类名就会被打印出来,这样我们就可以时刻知晓当前界面对应的是哪一个Activity了。
5.2 随时随地退出程序
如果手机的界面停留在ThirdActivity,我们会发现当前想退出程序是非常不方便的,需要连按3次Back键才行。按Home键只是把程序挂起,并没有退出程序。如果我们的程序需要注销或者退出的功能该怎么办呢?看来要有一个随时随地都能退出程序的方案才行。
其实解决思路也很简单,只需要用一个专门的集合对所有的Activity进行管理就可以了。下面我们就来实现一下。
新建一个单例类ActivityCollector作为Activity的集合,代码如下所示:
1 | package work.icu007.activitylifecycletest |
这里使用了单例类,是因为全局只需要一个Activity集合。在集合中,我们通过一个ArrayList来暂存Activity,然后提供了一个addActivity()方法,用于向ArrayList中添加Activity;提供了一个removeActivity()方法,用于从ArrayList中移除Activity;最后提供了一个finishAll()方法,用于将ArrayList中存储的Activity全部销毁。注意在销毁Activity之前,我们需要先调用activity.isFinishing来判断Activity是否正在销毁中,因为Activity还可能通过按下Back键等方式被销毁,如果该Activity没有正在销毁中,我们再去调用它的finish()方法来销毁它。
接下来修改 BaseActivity中的代码
1 | open class BaseActivity : AppCompatActivity() { |
在BaseActivity的onCreate()方法中调用了ActivityCollector的addActivity()方法,表明将当前正在创建的Activity添加到集合里。然后在BaseActivity中重写onDestroy()方法,并调用了ActivityCollector的removeActivity()方法,表明从集合里移除一个马上要销毁的Activity。
从此以后,不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll()方法就可以了。例如在ThirdActivity界面想通过点击按钮直接退出程序,只需将代码改成如下形式:
1 | class ThirdActivity : BaseActivity() { |
5.3 启动Activity的最佳写法
启动Activity的方法相信我们已经非常熟悉了,首先通过Intent构建出当前的“意图”,然后调用startActivity()或startActivityForResult()方法将Activity启动起来,如果有数据需要在Activity之间传递,也可以借助Intent来完成。
假设SecondActivity中需要用到两个非常重要的字符串参数,在启动SecondActivity的时候必须传递过来,那么我们很容易会写出如下代码:
1 | val intent = Intent(this, SecondActivity::class.java) |
虽然这样写是完全正确的,但是在真正的项目开发中经常会出现对接的问题。比如SecondActivity并不是由你开发的,但现在你负责开发的部分需要启动SecondActivity,而你却不清楚启动SecondActivity需要传递哪些数据。这时无非就有两个办法:一个是你自己去阅读SecondActivity中的代码,另一个是询问负责编写SecondActivity的同事。你会不会觉得很麻烦呢?其实只需要换一种写法,就可以轻松解决上面的窘境。
1 | companion object : BaseActivity() { |
在这里我们使用了一个新的语法结构companion object,并在companion object中定义了一个actionStart()方法。之所以要这样写,是因为Kotlin规定,所有定义在companionobject中的方法都可以使用类似于Java静态方法的形式调用。
接下来我们重点看actionStart()方法,在这个方法中完成了Intent的构建,另外所有SecondActivity中需要的数据都是通过actionStart()方法的参数传递过来的,然后把它们存储到Intent中,最后调用startActivity()方法启动SecondActivity。