Android 入门 | Activity 篇

5,002 阅读23分钟

1.什么是 Activity

Actuvity 是最容易吸引用户的地方,它是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用中可以包含0个或多个 Activity,但不包含任何 Activity 的应用程序是无法被用户看见的。

2.Activity 的基本用法

2.1 手动创建 Activity

点击 Empty Activity 创建名为 FirstActivity 的 Activity

勾选 Generate Layout File 表示会创建一个对应的布局文件,勾选Launcher Activity 表示会将 FirstActivity 设置为当前项目的主 Activity,点击Finish。

2.2 创建布局和加载布局

右键 app/src/main/res 目录->New->Directory,会弹出一个新建目录的窗口,现在这里创建一个名为 layout 的目录,然后对着 layout 目录右键->New->Layout resource file,优惠弹出一个新建布局资源文件的窗口,我们将这个布局文件命名为first_layout,根元素默认选择为 LinearLayout。

创建完成之后可以在右侧看到预览窗口,可以点击对应按钮切换到对应的页面,在代码区域会生成如下代码

<?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="match_parent"
    android:orientation="vertical">

</LinearLayout>

2.2.1 在布局中添加按钮

2.2.2 加载布局文件

class FirstActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 在此加载布局文件
        setContentView(R.layout.first_layout)
    }
}

项目中每添加一个元素都会在 R 文件中响应的生成一个资源 id,因此刚才添加的布局文件的 id 就已经添加到了 R 文件中,所以可以通过R.layout.first_layout找到 first_layout.xml 的 id,然后将这个值传入到 setContentView() 方法即可。

2.3 在 AndoirdManifest.xml 文件中注册

<?xml version="1.0" encoding="utf-8"?>
<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 的注册声明要放在 <application> 标签中,这里通过 <activity> 标签来对 Activity 进行注册。在<activity>中通过 name 属性指定具体注册哪一个 Activity,那么这里填入 .FirstActivity ,前面加 “.” 是因为最外层已经声明了 package 属性,在 name 的地方添加.FirstActivity 即可通过全类名找到 FirstActivity。

经过了前面的步骤已经注册了 Activity,但是还不能运行程序,因为需要配置 Activity,也就是说需要指定最先启动哪个 Activity。所以需要在 <activity> 标签中添加 intent-filter 标签,然后在 intent-filter 标签中添加 <action android:name="android.intent.action.MAIN" /> 和 <category android:name="android.intent.category.LAUNCHER" /> 两行声明即可,修改后的代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<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">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

这样,FirstActivity 就成了这个应用的主 Activity 了,点击应用图标最先打开的就是这个 Activity。但是如果没有在应用中声明任何一个 Activity 作为主 Activity,这个程序依然是可以安装的,只是无法在启动器中看到这个应用程序。这种程序通常作为第三方服务供其它应用在内部进行调用。

运行效果:

2.4 在程序中使用 Toast

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

2.4.1 定义弹出 Toast 的触发点

class FirstActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 在此加载布局文件
        setContentView(R.layout.first_layout)
        val button1: Button = findViewById(R.id.button1)
        button1.setOnClickListener {
            // 使用 Toast
            Toast.makeText(this, "You clicked Button 1", 
                Toast.LENGTH_SHORT).show()
        }
    }
}

在 Activity 中通过 findViewById 找到布局文件中定义的元素,由于该方法会返回一个继承自 View 的泛型对象,因此 Kotlin 无法推导出得到的是什么类型的控件,所以需要将 button 显式的声明为 Button 类型,接着通过 setOnClickListener 注册一个监听器,点击按钮就会触发 onClick() 方法,所以在此书写代码逻辑,创建 Toast 首先需要传入三个参数,第一个是 Context,在这里传入 this 即可,第二个参数是显示的文本内容,第三个参数是 Toast 显示的时长,点击按钮后会有如下效果:

上面的代码实现是通过findViewById找到的控件,但是当控件过多时就会频繁的写这段代码,在使用 Java 开发时因为无法避免这种写法所以产生了 ButterKnife 之类的第三方开源库,但是在 Kotlin 中这个问题就不复存在了,因为使用 Kotlin 编写的 Android 程序在 app/build.gradle 文件中引入了 kotlin-android-extensions 插件,这个插件会根据布局文件中定义的控件的 id 自动生成一个具有相同名字的变量,我们可以直接使用,从而替代 findViewById。

class FirstActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 在此加载布局文件
        setContentView(R.layout.first_layout)
//        val button1: Button = findViewById(R.id.button1)
        button1.setOnClickListener {
            // 使用 Toast
            Toast.makeText(this, "You clicked Button 1",
                Toast.LENGTH_SHORT).show()
        }
    }
}

2.5 在 Activity 中使用 Menu

因为手机屏幕不如电脑屏幕那么大,所以为了节省屏幕空间,Android 中提供了菜单这个功能,下面就来使用一下。

2.5.1 在 res 目录下创建 menu 文件夹

  • 创建 menu 类型的文件夹,并创建 menu resource file 命名为 main

  • 修改 main 的代码

    <?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>
    

    在这里添加两个菜单选项,其中 id 属性时这个选项的唯一标识符,title是显示的文本信息。

  • 在 FirstActivity 中重写方法

    /**
     * 创建 menu
     */
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        // 加载 menu 资源文件
        menuInflater.inflate(R.menu.main, menu)
        return true
    }
    
    /**
     * 通过 item.itemId 监听点击了哪个 item,对点击了的 item 做出提示
     */
    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
    }
    

    在onCreateOptionsMenu方法中需要加载菜单资源文件,并指明菜单项添加到哪个 menu 中,最后要返回 true,如果返回 false 菜单就无法显示了,除此之外还要根据用户点击了的 item 做对应的逻辑处理,所以需要重写 onOptionsItemSelected 方法,然后通过 itemId 判断,并做出提示。

2.6 销毁一个 Activity

销毁 Activity 只需要在对应的方法中调用 finish() 即可。

button1.setOnClickListener {
    finish()
}

点击按钮会触发监听事件,然后回调用 finish() 方法完成 Activity 的销毁。

3.使用 Intent 在 Activity 之间穿梭

Intent 是 Android 中各组件之间进行交互的一种重要方式,它不仅可以置名当前组件想执行的动作,还可以在不同的组件之间传递数据。Intent 一般可用于启动 Activity、启动 Service以及发送广播等场景。Intent 大致分为两种:显式和隐式。

3.1 使用显示 Intent

3.1.1 创建 SecondActivity 并修改布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".SecondActivity">

    <Button
        android:id="@+id/button2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 2" />
</LinearLayout>

3.1.2 修改 FirstActivity

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

在点击事件中首先需要创建一个 Intent 对象,然后在第一个参数中指出当前的环境上下文,第二个参数指定要跳转的页面,这里的SecondActivity::class.java 和 Java 中的 SecondActivity.class 作用一致。

3.2 使用隐式 Intent

隐式 Intent 要比显示 Intent 含蓄的多,他并不明确指定要启动哪个 Activity,而是通过指定 action 和 category 的信息,让系统去分析这个 Intent,并找出合适的 Activity 去启动。

  • 修改 SecondActivity 的配置信息
<activity android:name=".SecondActivity">
    <intent-filter>
        <action android:name="com.example.activitytest.ACTION_START" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>
  • 修改 Intent 的内容
button1.setOnClickListener {
    val intent = Intent("com.example.activitytest.ACTION_START")
    startActivity(intent)
}

要想使用隐式 Intent 启动 Activity,则必须匹配设置的 <action> 和 <category> ,在这里指定配置文件中 SecondActivity 中<action>标签的内容,由于 <category> 的值是默认的,所以在这里不需要指定。

注意:一个 Intent 中只可以指定一个 <action> 但是可以指定多个 <category>。

3.2.1 指定多个 category

button1.setOnClickListener {
    val intent = Intent("com.example.activitytest.ACTION_START")
    intent.addCategory("com.example.activitytest.MY_CATEGORY")
    startActivity(intent)
}

注意:在指定多个 category 的同时,不要忘记在配置文件中添加 <category>标签,否则会报错。

3.2.2 更多隐式 Intent 的用法

隐式 Intent 不仅可以用来启动自己的 Activity,还可以启动其它程序的 Activity,这就让多个应用程序之间有了共享的可能。

  • 使用 Intnet 代开百度

    button1.setOnClickListener {
        val intent = Intent(Intent.ACTION_VIEW)
        intent.data = Uri.parse("http://www.baidu.com")
        startActivity(intent)
    }
    

    在这里将 action 指定为 ACTION_VIEW,并将需要跳转的 URI 进行解析,设置到 data 属性中,可以实现点击按钮跳转到这个 URL 界面的效果。与此对应,还可以在 <intent-filter> 标签中再配置一个 <data> 标签,为了更精准的响应数据,还可以在 <data> 标签中设置如下属性:

    只有当 <data> 标签中指定的内容和 Intent 中携带的 Data 数据完全一致时,当前 Activity 才会响应该 Intent,不过 <data> 标签中一般不会指定过多的内容。

  • 使用 Intent 打开拨号页面

    button1.setOnClickListener {
        val intent = Intent(Intent.ACTION_DIAL)
        intent.data = Uri.parse("tel:10086")
        startActivity(intent)
    }
    

    将 action 指定为 DIAL,并设置号码即可。

3.2.3 向下一个 Activity 传递数据

  • 修改 FirstActivity 代码

    button1.setOnClickListener {
        val data = "Hello SecondActivity"
        val intent = Intent(this, SecondActivity::class.java)
        intent.putExtra("extra_data", data)
        startActivity(intent)
    }
    

    在这里使用 putExtra 方法一键值对的形式传递数据。

  • 修改 SecondActivity 代码

    val extraData = intent.getStringExtra("extra_data")
    Log.d("SecondActivity", "extra data is $extraData")
    

3.2.4 返回数据给上一个 Activity

  • 修改 FirstActivity
    button1.setOnClickListener {
        val intent = Intent(this, SecondActivity::class.java)
        startActivityForResult(intent, 1)
    }
    
  • 修改 SecondActivity
    button2.setOnClickListener { 
        val intent = Intent()
        intent.putExtra("data_return", "Hello FirstActivity")
        setResult(RESULT_OK, intent)
        finish()
    }
    
    这里需要在 setResult 方法中传入状态码和intent对象,并将 Activity 销毁掉
  • 在 FirstActivity 中重写方法
    /**
     * 使用 startActivityForResult 启动的 Activity 被销毁后会回调
     * 上一个 Activity 的 onActivityResult
     */
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            1 -> if (resultCode == Activity.RESULT_OK) {
                val returnedData = data?.getStringExtra("data_return")
                Log.d("FirstActivity", "returned data is $returnedData")
            }
        }
    }
    
    由于在一个 Activity 中可能会调用 startActivityForResult 方法去启动很多不同的 Activity,每个 Activity 返回的数据都会回调到 onActivityResult 方法,所以需要使用通过检查 requestCode 的值判断数据来源。

注意:按照上面的书写方式用户必须点击按钮才可以将数据返回,如果点击了 back 键则不会返回数据,要想改变这个情况,需要在 SecondActivity 中再重写一个方法。

  • 重写方法处理 back 键问题

    /**
     * 在用户点击返回键后回调的方法,
     * 用于解决用户点击返回键无法返回数据的问题
     */
    override fun onBackPressed() {
        val intent = Intent()
        intent.putExtra("data_return", "Hello FirstActivity")
        setResult(Activity.RESULT_OK, intent)
        finish()
    }
    

4.Activity 的生命周期

4.1 返回栈

Android 是使用任务来管理 Activity 的,一个任务就是一组存放在栈里的 Activity 的集合,这个栈也被称为返回栈(back stack)。栈是一种后进先出的数据结构,在默认情况下,每当我们启动新的 Activity,它就会在返回栈中入栈,并处于栈顶的位置。而每当我们按下 Back 键或调用 finish() 方法后,处于栈顶的 Activity 就会出栈,前一个入栈的 Activity 就会重新处在栈顶的位置,下图展示了返回栈如何管理 Activity 入栈出栈操作。

4.2 Activity 状态

  • 运行状态

    当一个 Activity 处于栈顶时,Activity 就处于运行状态,系统最不愿回收的就是处于运行状态的 Activity,因为这会带来非常差的用户体验。

  • 暂停状态

    当一个 Activity 不再处于栈顶的位置,但仍然可见,Activity 就进入了暂停状态。例如在 Activity 上面显示了一个对话框,但是并没有完全遮挡住 Activity,系统也不愿意回收这种 Activity,但是会在内存极低的情况下会考虑进行回收。

  • 停止状态

    当一个 Activity 不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种 Activity 保存相应的状态和成员变量,但是这并不是完全可靠的,当其它地方需要内存时,处于停止状态的 Activity 有可能会被系统回收。

  • 销毁状态

    一个 Activity 从返回栈中移除后就变成了销毁状态。系统最倾向于回收这种状态的 Activity,以保证手机的内存充足。

4.3 Activity 的生存期

Activity 类中定义了7个回调方法,覆盖了 Activity 生命周期的每一个环节,下面就来介绍一下这7个方法。

  • onCreate()

    该方法会在 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。

Activity 生命周期示意图:

4.4 体验 Activity 的生命周期

4.4.1 创建两个 Activity 并修改布局文件为一些标识

4.4.2 修改配置文件

将 DialogActivity 的主题设置为 Dialog 风格。

4.4.3 修改 MainActivity 的布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/startNormalActivity"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start NormalActivity" />
    
    <Button
        android:id="@+id/startDialogActivity"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start DialogActivity" />

</LinearLayout>

4.4.4 修改 MainActivity 代码

class MainActivity : AppCompatActivity() {

    private val tag = "MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(tag, "onCreate")
        setContentView(R.layout.activity_main)
        startNormalActivity.setOnClickListener{
            val intent = Intent(this, NormalActivity::class.java)
            startActivity(intent)
        }
        startDialogActivity.setOnClickListener{
            val intent = Intent(this, DialogActivity::class.java)
            startActivity(intent)
        }
    }

    override fun onStart() {
        super.onStart()
        Log.d(tag, "onStart")
    }

    override fun onResume() {
        super.onResume()
        Log.d(tag, "onResume")
    }

    override fun onPause() {
        super.onPause()
        Log.d(tag, "onPause")
    }

    override fun onStop() {
        super.onStop()
        Log.d(tag, "onStop")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(tag, "onDestroy")
    }

    override fun onRestart() {
        super.onRestart()
        Log.d(tag, "onRestart")
    }
}

程序启动后:

点击 normal button

点击返回按钮

点击 dialog button 后

点击返回按钮后

在 MainActivity 中点击 back 键

4.5 Activity 被回收了怎么办

当我们在 ActivityA中进行了一些操作后,需要跳转到 ActivityB,这时 ActivityA 就进入了停止状态,在内存不足时可能会被回收,当用户从 ActivityB 返回后,ActivityA 就会被重新创建,这样之前保存的数据也就没有了,这显然是不合理的。

在 Android 中提供了一个 onSaveInstanceState() 方法,会在 ActivityA 被回收之前调用,因此我们可以通过这个方法保存数据。保存的数据会存储在 Bundle 中,我们可以通过 Bundle 对象取出数据。

4.5.1 使用 Bundle 对象

  • 重写方法
    /**
     * 当 Activity 被销毁时会回调此方法,从而保存已存在的数据
     */
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        Log.d(tag, "onCreate")
        val tempData = "Something you Just typed"
        outState.putString("data_key", tempData)
    }
    
  • 修改 onCreate 方法
    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)
        }
    }
    
    这里在 Bundle 不为空的情况下获取其中存储的数据并进行打印。

5.Activity 的启动模式

Activity 的启动模式一共有4种,分别是:standard、singleTop、singleTask、singleInstance,具体使用哪种启动模式要根据项目特定的需求,我们可以在 AndroidManifest.xml 配置文件中通过给 <activity> 标签指定 android:launchMode 属性设置具体的启动模式。

5.1 standard

standard 模式是 Activity 默认的启动模式,在不进行显式指定的情况下,所有 Activity 都会自动使用这种启动模式,在 standard 模式下,每当启动一个新的 Activity,他就会在返回栈中入栈,并处于栈顶的位置,对于使用 standard 模式的 Activity,系统不会在乎 Activity 是否已经在返回栈中存在,每次启动都会创建一个该 Activity 的新实例。

5.1.1 演示 standard 模式

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.first_layout)
    button1.setOnClickListener {
        val intent = Intent(this, FirstActivity::class.java)
        startActivity(intent)
    }
}

在这里每点击一次按钮就会启动一个新的 Activity。

因为点击了3次按钮,所以又创建了3个新的 Activity,所以需要点击4次back键才可以退回到桌面

5.2 singleTop

上面的 standard 模式无论需要启动的 Activity 是否在栈顶,都会被重新创建,这或者有些不合理,但是使用 singleTop 模式会检查栈顶 Activity 是不是要启动的 Activity,如果是就不会再重新创建了。

5.2.1 修改配置文件

这样设置之后会发现无论怎么点击都不会新建新的 Activity,因为在这个模式下会先对需要启动的 Activity 进行检查,看看它是不是在栈顶。而且在这个模式下只需要点击一次 back 键就可以回退到桌面。

5.2.2 修改 FirstActivity 和 SecondActivity 的代码

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

button2.setOnClickListener {
    val intent = Intent(this, FirstActivity::class.java)
    startActivity(intent)
}

在这里只需要修改点击事件中需要跳转的页面。

在这里点击了两次按钮,第一次因为 SecondActivity 不在栈顶,所以会创建 SecondActivity,当再次点击时会发现 FirstActivity 不在栈顶,所以又会新建 FirstActivity,当点击返回时,会首先回退到 SecondActivity,再次点击会回退到 FirstActivity,再点击一次 back 才会返回到桌面。

5.3 singleTask

前面使用的 singleTop 模式会判断要启动的 Activity 是否在栈顶,如果不在会进行创建,但是这样也会造成多次创建的问题。但是使用 singleTask 模式就可以解决这个问题,它会判断返回栈中是否又要启动的 Activity 的实例,如果有则直接使用该实例,并将该 Activity 上的 Activity 实例全部出栈。

5.3.1 修改启动模式

5.3.1 在 FirstActivity 中重写方法

override fun onRestart() {
    super.onRestart()
    Log.d("FirstActivity", "onRestart")
}

5.3.2 在 SecondActivity 中重写方法

override fun onDestroy() {
    super.onDestroy()
    Log.d("SecondActivity", "onDestroy")
}

通过运行结果可以看出,在 SecondActivity 中启动 FirstActivity 时,首先会对返回栈中是否有 FirstActivity 的实例进行检查,在这里肯定是有的,所以就会将 SecondActivity 的实例出栈,将 FirstActivity 的实例放在栈顶,点击一次back键就可以退回到桌面。

5.4 singleInstance

singleInstance 启动模式和其它启动模式不同的是它会启动一个新的返回栈来管理这个 Activity(如果 singleTask 模式指定了不同的 taskAffinity,也会启动一个新的返回栈)。该启动模式主要是为了解决共享 Activity 实例的问题。

5.4.1 修改启动模式

5.4.2 在 Activity 中打印 taskId

  • FirstActivity
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.first_layout)
        Log.d("FirstActivity", "Task id is $taskId")
        button1.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            startActivity(intent)
        }
    }
    
  • SecondActivity
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d("SecondActivity", "Task id is $taskId")
        setContentView(R.layout.activity_second)
        button2.setOnClickListener {
            val intent = Intent(this, ThirdActivity::class.java)
            startActivity(intent)
        }
    }
    
  • ThirdActivity
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d("ThirdActivity", "Task id si $taskId")
        setContentView(R.layout.activity_third)
    }
    

根据运行结果可以看出 SecondActivity 的返回栈 id 和 其它两个不同,这说明 SecondActivity 处于单独的一个返回栈。

点击 back 键后会发现直接返回到了 FirstActivity 中,再按 back 会返回到 SecondActivity,再次返回才会退出应用程序。产生这种结果是因为 FirstActivity 和 ThirdActivity 处于同一个返回栈,当这个返回栈为空后于是就会显示另一个返回栈栈顶的 Activity。

6.Kotlin 课堂

6.1 标准函数 with、run、apply

Kotlin 的标准函数指的是 Standard.kt 文件中定义的函数,任何 Kotlin 代码都可以自由地调用所有的标准函数。

6.1.1 with 函数

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

val result = with(obj) {
    // 这里是 obj 的上下文
    "value" // with 函数的返回值
}

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

需求:有一个水果列表,现在我们想吃完所有水果,并将结果打印出来

  • 传统写法
    val list = listOf<String>("Apple", "Banana", "Orange", "Pear", "Grape")
    val builder = StringBuilder()
    builder.append("Start eating fruit. \n")
    for (fruit in list) {
        builder.append(fruit).append("\n")
    }
    builder.append("Ate all fruits")
    
    val result = builder.toString()
    println(result)
    
  • 使用 with 函数
    val list = listOf<String>("Apple", "Banana", "Orange", "Pear", "Grape")
    val result = with(StringBuilder()) {
        append("Start eating fruit. \n")
        for (fruit in list) {
            append(fruit).append("\n")
        }
        append("Ate all fruits.")
        toString()
    }
    println(result)
    
    这里在上下文的位置传入了 StirngBuilder 对象,那么接下来整个 Lambda 表达式的上下文就是 StringBuilder,在 Lambda 表达式中不需要写 builder.append() 或者 builder.toString(),只需要简写成 append() 和 toString() 即可,并且 Lambda 的最后一行代码会作为 with 函数的返回值。

6.1.2 run 函数

run 函数的使用场景和 with 函数的使用场景非常类似,只是稍微做了一些语法改动而已。首先 run 函数时不能直接调用的,必须要调用某个对象的 run 函数才行,其次 run 函数只接受一个 Lambda 参数,并且会在 Lambda 表达式中提供调用对象的上下文。其它方面和 with 函数是一样的,包括也会使用 Lambda 表达式中的最后一行代码作为返回值。

val result = obj.run {
    // 这里的 obj 是上下文
    "value" // run 函数的返回值
}

需求:使用 run 函数实现吃水果的代码

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

总体变化非常小,做出的改变只是需要使用对象调用 run 函数,参数只有 Lambda。

6.1.3 apply 函数

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

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

这里与 run 函数的不同体现在返回值需要自己在输出语句中调用,其它的都是类似的。

6.2 定义静态方法

静态方法在某些编程语言里面又叫做类方法,指的就是这种不需要创建实例就能调用的方法,所有主流的编程语言都会支持静态方法这个特性。

在 Java 中定义一个静态方法非常简单,只需要在方法上加一个 static 关键字即可

public class Util {
    public static void doAction() {
        System.out.println("do action");
    }
}

这是一个简单的工具类,这个类中的方法只需要使用Util.doAction()调用即可,因而静态方法非常适合编写工具类,以为工具类通常没有创建实例的必要,基本是全局通用的。

在 Kotlin 中要想实现这种功能则需要使用单例类的方式实现,比如上述的Util工具类使用 Kotlin 要这样写

object Util {
    fun doAction() {
        println("do action")
    }
}

虽然这里的 doAction() 方法并不是静态方法,但是我们仍然可以使用 Util.doAction() 的方式来调用,这就是单例类所带来的便利性。

但是这么写会将单例类中的所有方法变成类似于静态方法的调用方式,如果只希望让某一个方法变成静态方法的调用方式就可以使用 companion object 了

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

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

这里首相将 单例类改成了一个普通类,在普通类中的 doAction1() 方法必须要通过实例化 Util 类才可以调用,而在companion object 中定义的 doAction2() 方法则可以直接通过Util.doAction2()的形式进行调用。

不过,doAction2() 方法其实也并不是静态方法,companion object这个关键字实际上会在 Util 类的内部创建一个伴生类,而 doAction2() 方法就是定义在这个伴生类里面的实例方法。只是 Kotlin 会保证 Util 类始终只会存在一个伴生类对象,因此调用 Util.doAction2() 方法实际上就是调用了Util 类中伴生对象的 doAction2() 方法。

由此可见,Kotlin 确实没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似于静态方法调用的写法。如果确实需要定义真正二点静态方法的话,在 Kotlin 中可以以注解和顶层方法实现。

  • 注解方式

    class Util {
        fun doAction1() {
            println("do action1")
        }
    
        companion object {
            @JvmStatic
            fun doAction2() {
                println("do action2")
            }
        }
    }
    

    通过添加 @JvmStatic 注解就可以让 Kotlin 编译器将这个方法编译成真正的静态方法了。

    注意:@JvmStatic 注解只能加在单例类和companion object中的方法上。

  • 顶层方法

    顶层方法是指那些没有定义在任何类中的方法,比如 main() 方法,Kotlin 会将所有的顶层方法全部编译成静态方法,因此只要你定义了一个顶层方法,那么它一定是静态的。

    fun doSomething() {
        println("do something")
    }
    

    这个方法定义在一个新的 .kt 文件中,那么这个方法就是一个顶层方法。

    在 Kotlin 中要是想调用顶层方法,直接输入函数名即可。

    如果想在 Java 文件中调用这个方法这么写是调用不到的,因为 Java 中的方法必须写在类里。

    但是 Kotlin 的编译器会根据 Kotlin 文件的名称生成一个 Java 文件,文件名为 Kotlin文件名+Kt.java,所以可以像下面这种方式进行调用。