跳转到内容

2.2 实验:Activity 状态保存与页面跳转

1.1 本次实验的项目参数(供参考)

本次已提供初始工程,无需手动新建项目。以下参数是该项目的基本信息,导入后可在 Android Studio 中核对:

参数名
APP_NAMENavigationDemo
PACKAGE_NAMEcn.edu.sziit.android.navigationdemo
MIN_SDKAPI 24

1.2 下载实验记录表

实验记录表:点击下载作业提交文档


1.3 导入项目

本次实验已为你准备好初始项目,无需从零新建。请按以下步骤操作:

步骤 1:点击下方链接下载项目压缩包:

📦 下载 NavigationDemo.zip

步骤 2:将压缩包解压到合适目录。

⚠️ 解压路径要求:

  • 不能含有中文字符
  • 不能含有空格
  • 建议放在非 C 盘的专用目录,例如:D:\AndroidProjects\NavigationDemo

步骤 3:将解压后的项目导入 Android Studio,并完成 Gradle Sync。

参考《导入已有 Android 项目》完成导入与同步。

导入完成后的界面预览

本步骤通过一个简单的计数器演示旋转屏幕导致数据丢失的问题,以及如何用 onSaveInstanceState 解决它。

2.1 修改布局,添加计数器控件

打开 app/src/main/res/layout/activity_main.xml,在布局中添加计数显示控件和 +1 按钮:

app/src/main/res/layout/activity_main.xml
<LinearLayout ...>
...
<TextView
android:id="@+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前计数:0"
android:textSize="24sp" />
<Button
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="+1" />
</LinearLayout>

2.2 在 MainActivity.kt 中添加计数逻辑

onCreate 中追加 count 变量和点击事件:

app/src/main/java/cn/edu/sziit/android/navigationdemo/MainActivity.kt
class MainActivity : AppCompatActivity() {
private var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
binding.tvCount.text = "当前计数:$count"
binding.btnAdd.setOnClickListener {
count++
binding.tvCount.text = "当前计数:$count"
}
}
}

运行后点击几次 +1,让计数累加,然后旋转屏幕

观察现象:计数归零了。

启动 APP,点击 +1 按钮触发计数器变化
向右旋转屏幕,计数值归零

2.3 分析问题原因

旋转屏幕触发了 onDestroyonCreate,Activity 被完整重建。变量 count 是定义在 Activity 中的普通 Kotlin 变量,随 Activity 销毁而消失,重建后从 0 开始。

旋转屏幕前后的日志输出内容

2.4 使用 onSaveInstanceState 保存状态

MainActivity.kt 中重写 onSaveInstanceState,并修改 onCreate 以恢复:

app/src/main/java/cn/edu/sziit/android/navigationdemo/MainActivity.kt
class MainActivity : AppCompatActivity() {
...
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("COUNT", count)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
count = savedInstanceState?.getInt("COUNT") ?: 0
binding.tvCount.text = "当前计数:$count"
}
}

2.5 验证修复效果

重新运行,点击 +1 几次后旋转屏幕,计数应保持不变。

添加状态保存代码后,旋转屏幕计数值保持不变

本步骤新建 SecondActivity,为后续跳转实验做准备。

3.1 新建 SecondActivity

在项目目录结构中,右键点击 app/src/main/java/cn/edu/sziit/android/navigationdemo 包名目录,选择 New → Activity → Empty Activity,在弹出的配置窗口中填写:

┌──────────────────────────────────────────────────┐
│ Activity Name: SecondActivity │
│ Layout Name: activity_second │
│ Source Language: Kotlin │
│ │
│ ☐ Launcher Activity ← 不要勾选 │
└──────────────────────────────────────────────────┘

操作示意如下:

新建 SecondActivity
填写 SecondActivity 的配置

点击 Finish


3.2 确认 AndroidManifest.xml 中的注册信息

打开 AndroidManifest.xml,确认 SecondActivity 已被注册,且没有 <intent-filter>

app/src/main/AndroidManifest.xml
<manifest ...>
<application ...>
...
<activity
android:name=".SecondActivity"
android:exported="false" />
</application>
</manifest>

android:exported 控制该 Activity 能否被外部应用启动:false 表示仅限本应用内部调用;true 表示允许外部 Intent 启动(有 <intent-filter> 时必须设为 true)。

MainActivity 对比:SecondActivity 没有 <intent-filter>,说明它不是启动入口,只能由其他 Activity 通过 Intent 显式打开。


3.3 为 SecondActivity 添加基础布局

打开 activity_second.xml,将内容替换为:

app/src/main/res/layout/activity_second.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvCourse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="课程名:(未传入)"
android:textSize="18sp" />
</LinearLayout>

3.4 修改 SecondActivity.kt 使用 ViewBinding

打开 SecondActivity.kt,将布局加载方式改为 ViewBinding:

app/src/main/java/.../SecondActivity.kt
import ...
import cn.edu.sziit.android.navigationdemo.databinding.ActivitySecondBinding
class SecondActivity : AppCompatActivity() {
private lateinit var binding: ActivitySecondBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
binding = ActivitySecondBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
}

ActivitySecondBindingactivity_second.xml 自动生成。完成修改后,访问布局控件(例如 tvCourse)时,通过 binding.tvCourse 即可,无需 findViewById()

本步骤在 MainActivity 中添加跳转按钮,实现向 SecondActivity 传递数据。

4.1 在 activity_main.xml 中添加跳转按钮

btnAdd 按钮之后追加:

app/src/main/res/layout/activity_main.xml
<LinearLayout ...>
...
<Button
android:id="@+id/btnAdd"
... />
<Button
android:id="@+id/btnGoSecond"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="进入第二页" />
...
</LinearLayout>

4.2 在 MainActivity.kt 中实现跳转

onCreate 中追加按钮点击逻辑:

app/src/main/java/cn/edu/sziit/android/navigationdemo/MainActivity.kt
package cn.edu.sziit.android.navigationdemo
import android.content.Intent
import android.os.Bundle
...
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
binding.btnGoSecond.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("courseName", "Android应用开发基础")
startActivity(intent)
}
}
}

Intent 的作用Intent 是 Android 中用于描述”要做什么”的对象。这里用 Intent(this, SecondActivity::class.java) 明确指定打开 SecondActivity,并通过 putExtra 附带数据。


4.3 在 SecondActivity.kt 中接收数据并显示

SecondActivity.ktonCreate 末尾添加接收逻辑:

app/src/main/java/cn/edu/sziit/android/navigationdemo/SecondActivity.kt
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
val courseName = intent.getStringExtra("courseName")
binding.tvCourse.text = "课程名:$courseName"
}
}

运行后点击”进入第二页”,确认 SecondActivity 正确显示传入的课程名。

SecondActivity 可以显示传入的课程名

本步骤在 SecondActivity 中添加表单,让用户填写姓名和学号后返回,MainActivity 通过 ActivityResultLauncher 接收结果。

5.1 在 activity_second.xml 中新增表单和确认按钮

在已有的 tvCourse 之后,在 </LinearLayout> 之前追加:

app/src/main/res/layout/activity_second.xml
<LinearLayout ...>
...
<TextView
android:id="@+id/tvCourse"
... />
<EditText
android:id="@+id/etName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:hint="请输入姓名"
android:inputType="textPersonName" />
<EditText
android:id="@+id/etStudentId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="请输入学号"
android:inputType="number" />
<Button
android:id="@+id/btnConfirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="确认返回" />
...
</LinearLayout>

5.2 在 SecondActivity.kt 中设置返回数据

onCreate 末尾追加确认按钮的点击事件:

app/src/main/java/cn/edu/sziit/android/navigationdemo/SecondActivity.kt
package cn.edu.sziit.android.navigationdemo
import android.content.Intent
import android.os.Bundle
...
class SecondActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
binding.btnConfirm.setOnClickListener {
val resultIntent = Intent()
resultIntent.putExtra("name", binding.etName.text.toString())
resultIntent.putExtra("studentId", binding.etStudentId.text.toString())
setResult(RESULT_OK, resultIntent)
finish()
}
}
}

setResult 的作用:将结果码(RESULT_OK)和附带数据写回给调用者。调用 finish() 关闭当前 Activity 并返回。

如果用户直接按返回键,不经过确认按钮,系统会自动传递 RESULT_CANCELED,调用者据此区分用户是否完成了操作。


5.3 在 activity_main.xml 中添加结果显示区域

btnGoSecond 按钮之后追加:

app/src/main/res/layout/activity_main.xml
<LinearLayout ...>
...
<Button
android:id="@+id/btnGoSecond"
... />
<TextView
android:id="@+id/tvResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="返回结果将显示在这里" />
...
</LinearLayout>

5.4 在 MainActivity.kt 中接收返回数据

将原来的 startActivity(intent) 改为通过 ActivityResultLauncher 启动。

第一步:在 MainActivity 内、onCreate 函数之前定义 launcher:

app/src/main/java/cn/edu/sziit/android/navigationdemo/MainActivity.kt
package cn.edu.sziit.android.navigationdemo
import androidx.activity.result.contract.ActivityResultContracts
class MainActivity : AppCompatActivity() {
...
private val launcher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val name = result.data?.getStringExtra("name")
val studentId = result.data?.getStringExtra("studentId")
binding.tvResult.text = "返回的学生:$name$studentId)"
} else {
binding.tvResult.text = "用户取消了操作"
}
}
...
}

第二步:将 onCreate 中的 startActivity(intent) 替换为 launcher.launch(intent)

app/src/main/java/cn/edu/sziit/android/navigationdemo/MainActivity.kt
binding.btnGoSecond.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("courseName", "Android应用开发基础")
startActivity(intent)
launcher.launch(intent)
}

⚠️ registerForActivityResult 必须在 Activity 创建阶段(onCreate 之前)完成注册。不能放在按钮点击回调里,否则会抛出异常。


5.5 验证效果

运行后:

  1. 点击”进入第二页” → 进入 SecondActivity,确认课程名正确显示
  2. 填写姓名和学号,点击”确认返回” → 返回 MainActivity,确认 tvResult 显示接收到的姓名和学号
  3. 再次进入,直接按手机返回键 → 返回 MainActivity,确认 tvResult 显示”用户取消了操作”
MainActivity 可以获取 SecondActivity 返回的数据

6.1 旋转屏幕后计数归零

说明 onSaveInstanceState 尚未添加,或 onCreate 中未读取 savedInstanceState。 检查代码是否包含:

count = savedInstanceState?.getInt("COUNT") ?: 0

6.2 点击”进入第二页”后 App 崩溃

最常见原因是 SecondActivity 没有在 AndroidManifest.xml 中注册。打开 AndroidManifest.xml 确认有:

<activity android:name=".SecondActivity" />

如果没有,手动添加。通常 Android Studio 在 New Activity 时会自动注册,如果是手动创建 .kt 文件则需要自行添加。


6.3 registerForActivityResult 抛出 IllegalStateException

错误信息通常为 LifecycleOwners must call register before they are STARTED

原因:registerForActivityResult 被放在了按钮点击回调里或 onStart/onResume 中。必须将其移到 class 成员变量位置(onCreate 之前),确保在 Activity 启动前完成注册。


6.4 SecondActivity 返回后 tvResult 没有更新

检查以下两点:

  1. SecondActivity 中是否调用了 setResult(RESULT_OK, resultIntent) 然后 再调用 finish()
  2. MainActivity 中是否已将 startActivity 替换为 launcher.launch