2.2 实验:Activity 状态保存与页面跳转
1.1 本次实验的项目参数(供参考)
本次已提供初始工程,无需手动新建项目。以下参数是该项目的基本信息,导入后可在 Android Studio 中核对:
| 参数名 | 值 |
|---|---|
APP_NAME | NavigationDemo |
PACKAGE_NAME | cn.edu.sziit.android.navigationdemo |
MIN_SDK | API 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 按钮:
<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 变量和点击事件:
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,让计数累加,然后旋转屏幕。
观察现象:计数归零了。
2.3 分析问题原因
旋转屏幕触发了 onDestroy → onCreate,Activity 被完整重建。变量 count 是定义在 Activity 中的普通 Kotlin 变量,随 Activity 销毁而消失,重建后从 0 开始。
2.4 使用 onSaveInstanceState 保存状态
在 MainActivity.kt 中重写 onSaveInstanceState,并修改 onCreate 以恢复:
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 ← 不要勾选 │└──────────────────────────────────────────────────┘操作示意如下:
点击 Finish。
3.2 确认 AndroidManifest.xml 中的注册信息
打开 AndroidManifest.xml,确认 SecondActivity 已被注册,且没有 <intent-filter>:
<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,将内容替换为:
<?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:
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 } }}ActivitySecondBinding 由 activity_second.xml 自动生成。完成修改后,访问布局控件(例如 tvCourse)时,通过 binding.tvCourse 即可,无需 findViewById()。
本步骤在 MainActivity 中添加跳转按钮,实现向 SecondActivity 传递数据。
4.1 在 activity_main.xml 中添加跳转按钮
在 btnAdd 按钮之后追加:
<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 中追加按钮点击逻辑:
package cn.edu.sziit.android.navigationdemo
import android.content.Intentimport 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.kt 的 onCreate 末尾添加接收逻辑:
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ...
val courseName = intent.getStringExtra("courseName") binding.tvCourse.text = "课程名:$courseName" }}运行后点击”进入第二页”,确认 SecondActivity 正确显示传入的课程名。
本步骤在 SecondActivity 中添加表单,让用户填写姓名和学号后返回,MainActivity 通过 ActivityResultLauncher 接收结果。
5.1 在 activity_second.xml 中新增表单和确认按钮
在已有的 tvCourse 之后,在 </LinearLayout> 之前追加:
<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 末尾追加确认按钮的点击事件:
package cn.edu.sziit.android.navigationdemo
import android.content.Intentimport 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 按钮之后追加:
<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:
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):
binding.btnGoSecond.setOnClickListener { val intent = Intent(this, SecondActivity::class.java) intent.putExtra("courseName", "Android应用开发基础") startActivity(intent) launcher.launch(intent)}⚠️
registerForActivityResult必须在 Activity 创建阶段(onCreate之前)完成注册。不能放在按钮点击回调里,否则会抛出异常。
5.5 验证效果
运行后:
- 点击”进入第二页” → 进入 SecondActivity,确认课程名正确显示
- 填写姓名和学号,点击”确认返回” → 返回 MainActivity,确认
tvResult显示接收到的姓名和学号 - 再次进入,直接按手机返回键 → 返回 MainActivity,确认
tvResult显示”用户取消了操作”
6.1 旋转屏幕后计数归零
说明 onSaveInstanceState 尚未添加,或 onCreate 中未读取 savedInstanceState。
检查代码是否包含:
count = savedInstanceState?.getInt("COUNT") ?: 06.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 没有更新
检查以下两点:
SecondActivity中是否调用了setResult(RESULT_OK, resultIntent)然后 再调用finish()MainActivity中是否已将startActivity替换为launcher.launch