文章目录
本次我们会开发 多 Activity 的 APP。其中会涉及,启动一个新的 Activity,并从一个 Activity 向另一个的 Activity 传递数据。
我们添加一个新的Activity,可看到题目答案, 效果如下:
6.1 设计子Activity
首先在 strings.xml 中添加字符串资源:
<string name="warning_text">您确定要这样吗?</string>
<string name="show_answer_button">查看答案</string>
<string name="cheat_button">让我看一眼答案吧!</string>
<string name="judgment_toast">作弊是不对的.</string>
创建新的 CheatActivity,如下图所示:

填入新 Activity 的名称,如下:

6.1.1 设计布局
我们将 CheatActivity 的布局设计为如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context="com.bignerdranch.android.geoquiz.CheatActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="24dp"
android:text="@string/warning_text" />
<TextView
android:id="@+id/answer_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="24dp"
tools:text="Answer" />
<Button
android:id="@+id/show_answer_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/show_answer_button" />
</LinearLayout>
整体布局如下:

拓扑结构如下:

其中,点预览界面上方的工具栏,可以切换横竖屏的预览效果:

6.1.2 声明 manifest
在 AndroidStudio 添加 Activity 后,其会自动在 AndroidManifest.xml 中添加 CheatActivity 的声明:
<activity android:name=".CheatActivity" android:exported="false" />
这里的android:name属性是必需的。属性值前面的点号(.)告诉操作系统:activity类文件位于manifest配置文件头部包属性值指定的包路径下。
android:name属性值也可以设置成完整的包路径,比如android:name=“com.bignerdranch.android.geoquiz.CheatActivity”。
6.1.3 为 MainActivity 添加新按钮
添加一个 cheat_button 按钮,如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/question_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="24dp"
tools:text="@string/question_text" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/true_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/true_button" />
<Button
android:id="@+id/false_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/false_button" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageButton
android:id="@+id/prev_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/arrow_left" />
<ImageButton
android:id="@+id/next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/arrow_right" />
</LinearLayout>
<Button
android:id="@+id/cheat_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/cheat_button">
</Button>
</LinearLayout>
布局如下图所示:

6.2 启动子Activity
当调用StartActivity(Intent)时, 会发请求给操作系统的ActivityManager, 其会创建Activity实例并调用起onCreate(Bundle?)函数,代码如下:
private lateinit var cheatButton: Button
cheatButton = findViewById(R.id.cheat_button)
cheatButton.setOnClickListener {
val intent = Intent(this, CheatActivity::class.java)
startActivity(intent)
}
通信流程图如下:

Intent 的构造函数是 Intent(packageContext: Context, class: Class<?>),调用方式如下:
cheatButton.setOnClickListener {
// Start CheatActivity
val intent = Intent(this, CheatActivity::class.java)
startActivity(intent)
}
Intent 对象是 component 用来与操作系统通信的一种媒介工具。我们可用 intent 用来告诉 ActivityManager 该启动哪个 activity,因此可使用以下构造函数:Intent(packageContext: Context, class: Class<?>)
其中,传入 Intent 构造函数的 Class 类型参数,会告诉 ActivityManager 应该启动哪个 activity。其中 Context 参数告诉 ActivityManager 在哪里可以找到它。
在启动 activity 前,ActivityManager 会确认指定的 Class 是否已在 manifest 配置文件中声明:
- 如果已完成声明,则启动 activity,应用正常运行。
- 反之,则抛出 ActivityNotFoundException 异常,应用崩溃。
- 这就是必须在 manifest 配置文件中声明应用的全部 activity 的原因。
6.2.1 传递参数
可以把extra信息,附加在传入 startActivity(Intent) 函数的 Intent上发送出去。
传参的流程图如下:
CheatActivity 通过 伴生对象的newIntent() 创建 Intent,代码如下:
const val EXTRA_ANSWER_IS_TRUE = "com.bignerdranch.android.geoquiz.answer_is_true"
class CheatActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cheat)
}
companion object {
fun newIntent(packageContext: Context, answerIsTrue: Boolean): Intent {
return Intent(packageContext, CheatActivity::class.java).apply {
putExtra(EXTRA_ANSWER_IS_TRUE, answerIsTrue)
}
}
}
}
在 MainActivity,可通过调用 CheatActivity 的 newIntent() 方法,启动 CheatActivity 并向其传递参数,代码如下:
cheatButton.setOnClickListener {
val answerIsTrue = quizViewModel.currentQuestionAnswer
val intent = CheatActivity.newIntent(this@MainActivity, answerIsTrue)
startActivity(intent)
}
CheatActivity 被创建后,可接收传入的参数,并显示到对应的 TextView 控件上,代码如下:
package com.bignerdranch.android.geoquiz
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
const val EXTRA_ANSWER_IS_TRUE = "com.bignerdranch.android.geoquiz.answer_is_true"
class CheatActivity : AppCompatActivity() {
private lateinit var answerTextView: TextView
private lateinit var showAnswerButton: Button
private var answerIsTrue = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cheat)
answerIsTrue = intent.getBooleanExtra(EXTRA_ANSWER_IS_TRUE, false) // 题目的正确答案,从 MainActivity 传来
answerTextView = findViewById(R.id.answer_text_view)
showAnswerButton = findViewById(R.id.show_answer_button)
showAnswerButton.setOnClickListener {
val answerText = when {
answerIsTrue -> R.string.true_button
else -> R.string.false_button
}
answerTextView.setText(answerText)
}
}
companion object {
fun newIntent(packageContext: Context, answerIsTrue: Boolean): Intent {
return Intent(packageContext, CheatActivity::class.java).apply {
putExtra(EXTRA_ANSWER_IS_TRUE, answerIsTrue)
}
}
}
}
效果如下,当点击 “让我看一眼答案吧!”按钮 时,MainActivity 传递正确答案 到 CheatActivity,当在 CheatActivity 点击 “查看答案” 按钮时,在 TextView 处显示正确答案。

6.3 从子Activity返回结果
现在,用户可以毫无顾忌地偷看答案了。如果CheatActivity能把用户是否看过答案的情况通知给MainActivity就更好了。下面来解决这个问题。
通过 Activity.startActivityForResult(Intent, Int) 函数,可以获得子 Activity 的返回结果。
- 其第一个参数是 intent。
- 第二个参数是请求代码。请求代码是先发送给子 activity,然后再返回给父 activity 的整数值,由用户定义。当一个 activity 启动多个不同类型的子 activity,且需要判断消息回馈方时,就会用到该请求代码。
虽然 MainActivity 只启动一种类型的子 activity,但为应对未来的需求变化,现在就应设置请求代码常量。
private const val REQUEST_CODE_CHEAT = 0
startActivityForResult(intent, REQUEST_CODE_CHEAT)
在父 activity 需要依据子 activity 的完成结果采取不同操作时,设置结果代码就非常有用。
例如,假设子 activity 有一个 OK 按钮和一个 Cancel 按钮,并且每个按钮的点击动作分别设置有不同的结果代码。那么,根据不同的结果代码,父 activity 就能采取不同的操作。
子 Activity 可通过如下两种方式返回给父 Activity,其中 resultCode 可为一下常量之一(Activity.RESULT_OK 和 Activity.RESULT_CANCELED)。
setResult(resultCode: Int)
setResult(resultCode: Int, data: Intent)
CheatActivity 中,当用户点击 SHOW ANSWER 按钮时,CheatActivity 调用 setResult(Int, Intent) 函数将结果代码以及 intent 打包,代码如下:
showAnswerButton.setOnClickListener {
val answerText = when {
answerIsTrue -> R.string.true_button
else -> R.string.false_button
}
answerTextView.setText(answerText)
setAnswerShownButton(true)
}
private fun setAnswerShownButton(isAnswerShown: Boolean) {
val data = Intent().apply{
putExtra(EXTRA_ANSWER_SHOWN, isAnswerShown)
}
setResult(Activity.RESULT_OK, data)
}
当按Back键返回桌面时, ActivityManager会调用父Activity的 onActivityResult(requestCode: Int, resultCode: Int, data: Intent)函数, 如下图:

6.4 父Activity处理返回的结果
首先,我们在 QuizViewModel 中添加 isCheater 变量,代码如下:
package com.bignerdranch.android.geoquiz
import android.util.Log
import androidx.lifecycle.ViewModel
private const val TAG = "QuizViewModel"
class QuizViewModel : ViewModel() {
init {
Log.d(TAG, "ViewModel instance created")
}
override fun onCleared() {
super.onCleared()
Log.d(TAG, "ViewModel instance about to be destroyed")
}
val questionBank = listOf(
Question(R.string.question_australia, true),
Question(R.string.question_oceans, true),
Question(R.string.question_mideast, false),
Question(R.string.question_africa, false),
Question(R.string.question_americas, true),
Question(R.string.question_asia, true),
)
var currentIndex = 0
var isCheater = false
val currentQuestionAnswer: Boolean
get() = questionBank[currentIndex].answer
val currentQuestionText: Int
get() = questionBank[currentIndex].textResId
fun moveToNext(step: Int) {
val sz = questionBank.size
currentIndex = (currentIndex + step + sz) % sz
}
fun setCurrentQuestionAnswered() {
questionBank[currentIndex].answered = true
}
}
其次,在 MainActivity 中处理返回数据,首先处理 resultCode 判断是否正常,其次判断 requestCode 判断 CheatActivity 的业务意图,代码如下:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != Activity.RESULT_OK) {
return
}
if (requestCode == REQUEST_CODE_CHEAT) {
quizViewModel.isCheater = data?.getBooleanExtra(EXTRA_ANSWER_IS_TRUE, false) ?: false
}
}
private fun checkAnswer(userAnswer: Boolean) {
val correctAnswer = quizViewModel.currentQuestionAnswer
val messageResId = when {
quizViewModel.isCheater -> R.string.judgment_toast
userAnswer == correctAnswer -> R.string.correct_toast
else -> R.string.incorrect_toast
}
val t = Toast.makeText(this, messageResId, Toast.LENGTH_SHORT)
t.setGravity(Gravity.TOP, 0, 0)
t.show()
}
效果如下,当查看答案后,CheatActivity 向 MainActivity 传码,MainActivity 判断,并显示对应的 UI:

6.5 Activity的管理和使用
来看看当我们在各activity间往返的时候,操作系统层面到底发生了什么。首先,在桌面启动器中点击GeoQuiz应用时,操作系统并没有启动应用,而只是启动了应用中的一个activity。确切地说,它启动了应用的launcher activity。在 GeoQuiz 应用中,MainActivity就是它的launcher activity。
使用应用向导创建 GeoQuiz 应用以及 MainActivity 时,MainActivity 默认被设置为 launcher activity。配置文件中,MainActivity 声明的intent-filter元素节点下,可看到 MainActivity 被指定为 launcher activity。AndroidManifest.xml 如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.bignerdranch.android.geoquiz">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GeoQuiz"
tools:targetApi="31">
<activity
android:name=".CheatActivity"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
各 Activity 的栈关系如下图:

按回退键 或 在CheatActivity中调用Activity.finish()函数,都可以把 CheatActivity实例弹出栈外,使 MainActivity 重新回到栈顶部。
如果运行GeoQuiz应用,在MainActivity界面按回退键,MainActivity将从栈里弹出,我们将退回到GeoQuiz应用运行前的画面, 如下两张图
如果从桌面启动器启动GeoQuiz应用,在MainActivity界面按回退键,将退回到桌面启动器界面,如下图:

其实, ActivityManager 维护着一个非特定应用独享的后退栈。所有应用的activity都共享该后退栈。这也是将ActivityManager设计成 操作系统级的 activity管理器,来负责启动应用activity的原因之一。显然,后退栈是作为一个整体共享于操作系统及设备,而不单单用于某个应用。
6.6 挑战:堵住作弊漏洞
现在,GeoQuiz 应用有个大漏洞。用户作弊后,可以旋转CheatActivity来清除作弊痕迹,然后回到MainActivity界面,假装什么也没发生过,效果如下:

我们希望在设备旋转或进程销毁时,设法保存 CheatActivity 的 UI 状态数据,堵住这个漏洞。
需求其实是数据持久化,我们可用 ViewModel 或 onSaveInstanceState 实现,我们这次选 onSaveInstanceState。
当 CheatActivity 的 showAnswerButton 按钮被点击时,置位 cheatViewModel.isAnswerShown 状态,代码如下:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cheat)
if (savedInstanceState != null) {
cheatViewModel.isAnswerShown = savedInstanceState.getBoolean(isAnswerShowedKey, false)
}
answerIsTrue = intent.getBooleanExtra(EXTRA_ANSWER_IS_TRUE, false) // 题目的正确答案
answerTextView = findViewById(R.id.answer_text_view)
showAnswerButton = findViewById(R.id.show_answer_button)
showAnswerButton.setOnClickListener {
val answerText = when {
answerIsTrue -> R.string.true_button
else -> R.string.false_button
}
answerTextView.setText(answerText)
cheatViewModel.isAnswerShown = true
setAnswerShowResult(cheatViewModel.isAnswerShown)
}
}
当旋转屏幕时,CheatActivity 在 Stop 之前会自动调用 onSaveInstanceState 回调函数存储状态,代码如下:
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
savedInstanceState.putBoolean(isAnswerShowedKey, cheatViewModel.isAnswerShown)
}

此时,当用户按下 Back 键(企图假装并没有作弊偷看答案时)回到 MainActivity 时,虽然 CheatActivity 已被 Stop,但 CheatViewModel 的数据一直常驻内存,则可通过 onBackPressed() 从 CheatActivity 返回 MainActivity,代码如下:
override fun onBackPressed() {
super.onBackPressed()
setAnswerShowResult(cheatViewModel.isAnswerShown)
}
针对,MainActivity,首先引入 CheatViewModel,代码如下: