1. 欢迎
此实用 Codelab 是“Android 开发者基础知识(第 2 版)”课程中的“第 1 单元:使用入门”的一部分。如果您按顺序学习这些 Codelab,将能充分利用本课程:
- 如需查看课程中 Codelab 的完整列表,请参阅“Android 开发者基础知识 (V2)”Codelab。
- 如需详细了解本课程(包括指向所有概念章节、应用和幻灯片的链接),请参阅“Android 开发者基础知识(版本 2)”。
简介
在本次实践课中,您将详细了解 activity 生命周期。生命周期是 activity 从始至终(从创建到被销毁以及系统收回其资源)可以处于的一组状态。当用户在应用中的 activity 之间导航(以及进入和退出应用)时,这些 activity 都会在它们生命周期的不同状态之间转换。
activity 生命周期中的每个阶段都有一个对应的回调方法:onCreate()、onStart()、onPause() 等。当 activity 更改状态时,系统会调用关联的回调方法。您已经了解过以下方法之一:onCreate()。通过替换 Activity 类中的任何生命周期回调方法,您可以更改 activity 的默认行为来响应用户或系统操作。
activity 状态也可能会为响应设备配置变化而更改(例如,当用户将设备从竖屏旋转为横屏时)。当发生这些配置更改时,系统会销毁 activity,并以其默认状态重新创建它,而用户可能会丢失在 activity 中输入的信息。为避免让您的用户感到困惑,请务必在开发应用时做好数据意外丢失防护工作。在本次实践课的后面部分中,您将对配置更改进行实验,了解如何保留为响应设备配置更改和其他 activity 生命周期事件而创建的 activity 的状态。
在本次实践课中,您将日志记录语句添加到 TwoActivities 应用,并在使用该应用时观察 activity 生命周期的变化。然后,开始处理这些更改,并探索在此类情况下如何处理用户输入。
前提条件
您应该能够:
- 在 Android Studio 中创建并运行应用项目。
- 将日志语句添加到应用,并在 Logcat 窗格中查看这些日志。
- 了解和使用 activity 和 intent,并能够轻松自如地与它们互动。
学习内容
- activity 生命周期的运作方式。
- 何时启动、暂停、停止和销毁 activity。
- 与 activity 更改关联的生命周期回调方法简介。
- 可能导致 activity 生命周期事件的操作(例如配置更改)的影响。
- 如何在生命周期事件中保留 activity 状态。
您将执行的操作
- 将代码添加到上一实践课程中的 TwoActivities 应用,实现各种 activity 生命周期回调,以添加日志记录语句。
- 观察在应用运行期间以及您与应用中的每个 activity 互动时,状态如何变化。
- 修改应用,以保留为响应用户行为或设备上的配置更改而意外重新创建的 activity 的实例状态。
2. 应用概览
在本次实践课中,您将向 TwoActivities 应用添加内容。该应用的外观和行为与它在上一个 Codelab 中的外观和行为大致相同。它包含两个 activity 实现,让用户能够在两者之间发送内容。您在本次实践课中对应用所做的更改不会影响其可见的用户行为。
3. 3. 任务 1:向 TwoActivities 添加生命周期回调
在此任务中,您将实现所有 activity 生命周期回调方法,以便在调用这些方法时向 Logcat 输出消息。通过这些日志消息,您可以了解 activity 生命周期状态何时发生更改,以及这些生命周期状态更改会对应用运行时产生什么影响。
1.1(可选)复制 TwoActivities 项目
对于本次实践课中的任务,您将修改在上一实践课程中构建的现有 TwoActivities 项目。如果您希望将之前的 TwoActivities 项目保持不变,请按照附录:实用程序中的步骤创建项目的副本。
1.2 在 MainActivity 中实现回调
- 在 Android Studio 中打开 TwoActivities 项目,然后在“Project”>“Android”窗格中打开 MainActivity。
- 在 onCreate() 方法中,添加以下日志语句:
Log.d(LOG_TAG, "-------");
Log.d(LOG_TAG, "onCreate");
- 为 onStart() 回调添加替换项,并通过一个语句将该事件记录到日志中:
@Override
public void onStart(){
super.onStart();
Log.d(LOG_TAG, "onStart");
}
快捷的方式是在 Android Studio 中依次选择“Code”>“Override Methods”。系统会显示一个对话框,其中列出了您可以在类中替换的所有可能的方法。从列表中选择一个或多个回调方法将会插入这些方法的完整模板,包括对父类的所需调用。
- 使用 onStart() 方法作为模板来实现 onPause()、onRestart()、onResume()、onStop() 和 onDestroy() 生命周期回调
所有回调方法都具有相同的签名(名称除外)。如果您通过复制和粘贴 onStart() 来创建这些其他回调方法,请记得更新内容以调用父类中的正确方法,并记录正确的方法。
- 运行应用。
- 点击 Android Studio 底部的“Logcat”标签页以显示 Logcat 窗格。您应该会看到三条日志消息,其中显示了 activity 在启动后所经历的三种生命周期状态:
D/MainActivity: -------
D/MainActivity: onCreate
D/MainActivity: onStart
D/MainActivity: onResume
1.3 在 SecondActivity 中实现生命周期回调
现在,您已为 MainActivity 实现了生命周期回调方法,请为 SecondActivity 执行相同的操作。
- 打开 SecondActivity。
- 在类的顶部,为 LOG_TAG 变量添加一个常量:
private static final String LOG_TAG = SecondActivity.class.getSimpleName();
- 将生命周期回调和日志语句添加到第二个 activity。(您可以从 MainActivity 复制并粘贴回调方法。)
- 将一个日志语句添加到 returnReply() 方法中,并紧挨着放在 finish()方法之前:
Log.d(LOG_TAG, "End SecondActivity");
1.4 在应用运行时观察日志**
- 运行应用。
- 点击 Android Studio 底部的“Logcat”标签页以显示 Logcat 窗格。
- 在搜索框中输入 活动 。Android logcat 可能会很长并且杂乱无章。由于每个类中的 LOG_TAG 变量包含单词 MainActivity 或 SecondActivity,因此您可以使用关键字过滤日志,以便仅显示您感兴趣的内容。
使用您的应用进行实验,并注意为了响应不同操作而发生的生命周期事件。具体来说,请尝试以下操作:
- 正常使用应用(发送一条消息,用另一条消息进行回复)。
- 使用返回按钮从第二个 Activity 返回主 Activity。
- 使用应用栏中的向上箭头从第二个 activity 返回主 activity。
- 在应用中的不同时间在主 activity 和第二个 activity 上旋转设备,并在日志中和屏幕上观察 * 中发生的变化。
- 按“概览”按钮(主屏幕右侧的方形按钮),然后关闭应用(点按 X)。
- 返回主屏幕,然后重启您的应用。
提示:如果您是在模拟器中运行应用,可以使用 Ctrl+F11 或 Control+Function+F11 来模拟旋转。
任务 1 解决方案代码
以下代码段显示了第一个任务的解决方案代码。
MainActivity
以下代码段显示了 MainActivity 中添加的代码,但并未显示整个类。
onCreate() 方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Log the start of the onCreate() method.
Log.d(LOG_TAG, "-------");
Log.d(LOG_TAG, "onCreate");
// Initialize all the view variables.
mMessageEditText = findViewById(R.id.editText_main);
mReplyHeadTextView = findViewById(R.id.text_header_reply);
mReplyTextView = findViewById(R.id.text_message_reply);
}
其他生命周期方法:
@Override
protected void onStart() {
super.onStart();
Log.d(LOG_TAG, "onStart");
}
@Override
protected void onPause() {
super.onPause();
Log.d(LOG_TAG, "onPause");
}
@Override
protected void onRestart() {
super.onRestart();
Log.d(LOG_TAG, "onRestart");
}
@Override
protected void onResume() {
super.onResume();
Log.d(LOG_TAG, "onResume");
}
@Override
protected void onStop() {
super.onStop();
Log.d(LOG_TAG, "onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(LOG_TAG, "onDestroy");
}
SecondActivity
以下代码段显示了 SecondActivity 中添加的代码,但并未显示整个类。
在 SecondActivity 类的顶部:
private static final String LOG_TAG = SecondActivity.class.getSimpleName();
returnReply() 方法:
public void returnReply(View view) {
String reply = mReply.getText().toString();
Intent replyIntent = new Intent();
replyIntent.putExtra(EXTRA_REPLY, reply);
setResult(RESULT_OK, replyIntent);
Log.d(LOG_TAG, "End SecondActivity");
finish();
}
其他生命周期方法:
与上面的 MainActivity 相同。
4. 4. 任务 2:保存和恢复 Activity 实例状态
根据系统资源和用户行为,应用中每个 Activity 的销毁和重建频率可能会超出您的想象。
您可能在上一部分中旋转设备或模拟器时注意到了此行为。旋转设备就是设备配置更改的一个示例。虽然旋转是最常见的配置变更,但所有配置变更都会导致当前 activity 被销毁并重新创建为新的 activity。如果您在代码中未考虑这种行为,当发生配置更改时,您的 Activity 布局可能会还原为默认外观和初始值,并且您的用户可能会丢失其在应用中的位置、数据或进度状态。
每个 Activity 的状态均以一组键值对的形式存储在名为“Activity 实例状态”的 Bundle 对象中。系统会在 Activity 停止之前将默认状态信息保存到实例状态 Bundle,并将该 Bundle 传递给新的 Activity 实例进行恢复。
为了避免 Activity 在被意外销毁并重新创建时丢失数据,您需要实现 onSaveInstanceState() 方法。如果有可能销毁并重新创建 Activity,系统会对您的 Activity(在 onPause() 和 onStop() 之间)调用此方法。
您在实例状态下保存的数据仅适用于当前应用会话期间的这一特定 activity 实例。当您停止并重启新的应用会话时,activity 实例状态会丢失,并且 activity 会还原为默认外观。如果您需要在应用会话之间保存用户数据,请使用共享偏好设置或数据库。我们会在后面的实践课程中介绍这两种情况。
2.1 使用 onSaveInstanceState() 保存 activity 实例状态
您可能已经注意到,旋转设备完全不会影响第二个 activity 的状态。这是因为第二个 activity 布局和状态是根据布局和激活它的 intent 生成的。即使重新创建了 activity,该 intent 也仍然存在,并且每次调用第二个 activity 中的 onCreate() 方法时,仍会使用该 intent 中的数据。
此外,您可能会注意到,在每个 Activity 中,您在消息或回复 EditText 元素中输入的任何文本都会保留下来,即使设备旋转也是如此。这是因为在配置更改后,系统会自动保存布局中某些 View 元素的状态信息,而 EditText 的当前值就是其中之一。
因此,您感兴趣的唯一 Activity 状态是回复标头的 TextView 元素以及主 Activity 中的回复文本。默认情况下,两个 TextView 元素均不可见;只有当您从第二个 Activity 将消息发送回主 Activity 后,它们才会显示。
在此任务中,您将添加代码以使用 onSaveInstanceState() 保留这两个 TextView 元素的实例状态。
- 打开 MainActivity。
- 将此 onSaveInstanceState() 框架实现添加到 Activity,或使用“代码”>“替换方法”插入框架替换项。
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
- 检查标头当前是否可见,如果可见,则使用 putBoolean() 方法和键“reply_visible”将可见性状态放入状态 Bundle 中。
if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
outState.putBoolean("reply_visible", true);
}
请记住,在第二个 Activity 有回复之前,回复标题和文本将被标记为不可见。如果该标题可见,则表示有需要保存的回复数据。请注意,我们只对可见性状态感兴趣 - 无需保存标题的实际文本,因为文本永远不会更改。
- 在同一检查中,将回复文本添加到 Bundle 中。
outState.putString("reply_text",mReplyTextView.getText().toString());
如果标题可见,则可以认为回复消息本身也可见。您无需测试或保存回复消息的当前可见性状态。只有消息的实际文本会进入带有“reply_text”键的状态 Bundle。
您可以仅保存可能会在创建 Activity 后发生变化的 View 元素的状态。应用中的其他 View 元素(EditText、Button)可以随时从默认布局重新创建。
请注意,系统会保存某些 View 元素的状态,如 EditText 的内容。
2.2 在 onCreate() 中恢复 activity 实例的状态
保存 activity 实例状态后,您还需要在重新创建 activity 时恢复该状态。为此,您可以在 onCreate() 中执行此操作,也可以通过实现 onRestoreInstanceState() 回调(在创建 Activity 后调用 onStart() 后调用)来实现。
大多数情况下,恢复 Activity 状态的较好位置是在 onCreate() 中,以确保界面(包括状态)尽快可用。有时,在完成所有初始化后,在 onRestoreInstanceState() 中执行此操作非常方便,或者可以让子类决定是否使用您的默认实现。
- 在 onCreate() 方法中,使用 findViewById() 初始化 View 变量后,添加一项测试以确保 savedInstanceState 不为 null。
// Initialize all the view variables.
mMessageEditText = findViewById(R.id.editText_main);
mReplyHeadTextView = findViewById(R.id.text_header_reply);
mReplyTextView = findViewById(R.id.text_message_reply);
// Restore the state.
if (savedInstanceState != null) {
}
创建 Activity 后,系统会将状态 Bundle 作为其唯一参数传递给 onCreate()。首次调用 onCreate() 且您的应用启动时,Bundle 为 null,因为您的应用首次启动时没有现有状态。对 onCreate() 的后续调用将用您存储在 onSaveInstanceState() 中的数据填充一个捆绑包。
- 在该检查中,使用键“reply_visible”从 Bundle 中获取当前可见性(true 或 false)。
if (savedInstanceState != null) {
boolean isVisible =
savedInstanceState.getBoolean("reply_visible");
}
- 在上一行下方为 isVisible 变量添加一个测试。
if (isVisible) {
}
如果状态 Bundle 中有 Reply_visible 键(因此 isVisible 为 true),您需要恢复该状态。
- 在 isVisible 测试内,使标题可见。
mReplyHeadTextView.setVisibility(View.VISIBLE);
- 从 Bundle 获取带有“reply_text”键的文本回复消息,并设置回复 TextView 以显示该字符串。
mReplyTextView.setText(savedInstanceState.getString("reply_text"));
- 将回复 TextView 也设为可见:
mReplyTextView.setVisibility(View.VISIBLE);
- 运行应用。尝试旋转设备或模拟器,确保回复消息(如果有)在重新创建 activity 后仍然存在。
任务 2 解决方案代码
以下代码段显示了此任务的解决方案代码。
MainActivity
以下代码段显示了 MainActivity 中添加的代码,但并未显示整个类。
onSaveInstanceState() 方法:
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// If the heading is visible, message needs to be saved.
// Otherwise we're still using default layout.
if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
outState.putBoolean("reply_visible", true);
outState.putString("reply_text",
mReplyTextView.getText().toString());
}
}
onCreate() 方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(LOG_TAG, "-------");
Log.d(LOG_TAG, "onCreate");
// Initialize all the view variables.
mMessageEditText = findViewById(R.id.editText_main);
mReplyHeadTextView = findViewById(R.id.text_header_reply);
mReplyTextView = findViewById(R.id.text_message_reply);
// Restore the saved state.
// See onSaveInstanceState() for what gets saved.
if (savedInstanceState != null) {
boolean isVisible =
savedInstanceState.getBoolean("reply_visible");
// Show both the header and the message views. If isVisible is
// false or missing from the bundle, use the default layout.
if (isVisible) {
mReplyHeadTextView.setVisibility(View.VISIBLE);
mReplyTextView.setText(savedInstanceState
.getString("reply_text"));
mReplyTextView.setVisibility(View.VISIBLE);
}
}
}
完整的项目:
Android Studio 项目:TwoActivitiesLifecycle
5. 编码
挑战:创建一个简单的购物清单应用,其中主 activity 是用户正在构建的清单,第二个 activity 是常见购物清单。
- 主 activity 应包含要构建的列表,该列表应由 10 个空 TextView 元素组成。
- 主 activity 上的“Add Item”按钮可启动第二个 activity,其中包含常见购物清单(奶酪、大米、苹果等)。使用 Button 元素显示项目。
- 选择某个项后,用户会返回主 activity,并更新空 TextView 以包含选中的项。
使用 Intent 将信息从一个 Activity 传递到另一个 Activity。确保在用户旋转设备时,系统会保存购物清单的当前状态。
6. 总结
- activity 生命周期是 activity 会迁移的一组状态,从首次创建开始,到 Android 系统收回该 activity 的资源时结束。
- 当用户从一个 activity 导航到另一个 activity,以及从应用内部和外部导航时,每个 activity 都会在 activity 生命周期的不同状态之间移动。
- activity 生命周期中的每种状态都有一个对应的回调方法,您可以在 activity 类中替换此类方法。
- 生命周期方法包括 onCreate()、onStart()、onPause()、onRestart()、onResume()、onStop()、onDestroy()。
- 通过替换生命周期回调方法,您可以添加在 activity 转换为该状态时发生的行为。
- 您可以在 Android Studio 中依次选择“代码”>“替换”,为类添加框架替换方法。
- 设备配置更改(例如旋转)会导致 activity 被销毁,并重新创建为新的 activity。
- 在发生配置更改时,系统会保留一部分 activity 状态,包括 EditText 元素的当前值。对于所有其他数据,您必须自行明确保存它们。
- 在 onSaveInstanceState() 方法中保存 activity 实例状态。
- 实例状态数据以简单的键值对形式存储在 Bundle 中。使用 Bundle 方法将数据放入 Bundle 或从中获取数据。
- 可在 onCreate()(首选方式)或 onRestoreInstanceState() 中恢复实例状态。返回