原文:Introduction to Android Activities Tutorial
作者:Namrata Bandekar
译者:kmyhy
在你编写 Android app 时,你谋划的第一件事是如何征服全世界。开个玩笑。实际上,第一件事情是创建一个 activity。它是所有事情发生的地方,因为它们就是用户和你的 app 交互的界面。
简单说,activity 是构建 Android app 的砖石。
在本教程中,你将了解如何和 activity 打交道。你会创建一个 todo app,叫做 Forget Me Not。通过它你会学到:
- ativity 的创建、启动和终止过程,以及在两个 activity 之间进行导航。
- 在 activity 生命周期的每个阶段,以及在它生命周期中的每一阶段如何优雅地进行处理。
- 如何处理 activity 中的设置变化和持久化数据。
本教程假设你熟悉基本的 Android 开发。如何你没学过 Java、XML 或 Android Studio,请先阅读Android Tutorial for Beginners Series。
开始
从这里下载本教程的开始项目。打开 Android Studio,选择 Open an Existing Android Studio Project(打开已有的 Android Studio 项目)。
找到你所下载的 demo 项目,点击 Choose。
你接下来的主题和新朋友是 Forget Me Not,这是一个 demo app,它的主要功能是允许你添加、删除任务列表中的任务。它还显示了当前日期和时间,以便你能随时掌控全局。
运行程序,你会看到非常简单的界面:
这时,你还不能做什么。todo 列表是空的,你点击 add a task 按钮不会有什么反应。你的任务是编写一个可编辑的列表,让这个界面更有趣一点。
Activity 的生命周期
在开始编写代码之前,来一点理论知识。
先前提过,activity 是构建 app 界面的砖石。它们包含了多个用户能够交互的组件,很可能你的 app 需要多个 activity,以便对你创建的多个界面进行处理。
所有这些 activity 分别用于处理不同的任务,听起来,开发 Android app 真是一个不简单的任务。
幸运的是,Android 定义了一些回调方法,当它需要的时候回调用这些代码。这套机制就是 activity 的生命周期。
要创建一个稳定可靠的 app,关键就在于处理好 activity 的生命周期。activity 的生命周期如下图所示,它是一个阶梯式金字塔,每个阶段都会和核心的回调方法绑定:
根据上图,你可以将这个生命周期看成你的编码过程。详细介绍一下每个回调方法:
- onCreate(): 第一次创建 activity 时,会调用这个方法。你可以用这个方法初始化任何 UI 元素和数据对象。你还可以使用 activity 的 saveInstanceState,它保存了 activity 之前的状态,通过 saveInstanceState 你可以恢复 activity 的状态。
- onStart(): 这个方法在呈现 activity 给用户之前调用。在它后面调用的是 onResume() 方法,非常罕见的会是 onStop() 方法。你可以在这个方法中开始 UI 动画,播放音频或者其它需要在屏幕上出现的 activity 的内容。
- onResume(): 在将一个 activity 移到前台之前调用。你可以在这个方法中重新播放动画、更新 UI 元素、重启相机预览、恢复音频/视频播放或者初始化你在 onPause() 中释放的组件。
- onPause(): 在进入后台之前调用。在这个方法中,你应该停止和 activity 有关的视觉和音效,如 UI 动画、音乐回放或相机。如果 activity 返回前台,则这个方法后面会调用 onResume 方法,如果 activity 变成隐藏,则这个方法后面会调用 onStop()。
- onStop(): 这个方法在 onPause() 之后、activity 进入后台之前调用。这个方法是保存数据到磁盘的好地方。如果 activity 回到前台,它后面是 onRestart() 方法,如果它即将从内存移除,则它后面是 onDestroy() 方法。
- onRestart(): 在关闭一个 activity 但又重新打开它之前调用。它后面总是 onStart() 方法。
- onDestroy(): 这是 activity 被销毁之前你能从系统接收到的最后一个回调。销毁一个 activity 的方法之一,就是调用 finish() 方法。当系统需要回收内存时,也会销毁 activity。如果你的 activity 包含有后台线程或者需要长时间运行的资源,销毁它可能会导致内存泄漏——如果你没有释放这些资源的话。因此需要记住在这个方法中停止这些占用。
要记住的方法太多了!在下一节,你将会用到一些生命周期方法,你会更容易记住每个方法的用途。
创建一个 Activity
将 activity 的生命周期牢记于心,我们来看一个 demo 项目中的 activity。打开 MainActivity, 你会看到这样的 onCreate 方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
// 1
super.onCreate(savedInstanceState);
// 2
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
// 3
setContentView(R.layout.activity_main);
// 4
mDateTimeTextView = (TextView) findViewById(R.id.dateTimeTextView);
final Button addTaskBtn = (Button) findViewById(R.id.addTaskBtn);
final ListView listview = (ListView) findViewById(R.id.taskListview);
mList = new ArrayList<String>();
// 5
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mList);
listview.setAdapter(mAdapter);
// 6
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
}
});
}
代码解释如下:
- 调用父类的 onCreate() 方法。记住这必须是这个方法中的第一句。
- 告诉 WindowManager,让你的 activity 占据全屏。
- 用相应的布局文件作为 activity 的内容视图。
- 这里初始化所有的 UI 和数据变量。其中,用一个 TextView 显示当前日期和时间,一个按钮用于添加任务,一个 ListView 用于显示待办列表。一个 ArrayList 用于保存数据。你可以在 activity_main.xml 中找到所有这些 UI 元素的实现。
- 这里初始化了一个适配器,并用于处理 ListView 的数据。
- 用一个 OnItemClickListener() 作为 ListView 处理用户点击 List 的某个行的监听器。
注意:要了解关于 ListView 和 Adapter 的更多细节,请参考 Android 开发者文档。
现在你已经看到了,这个方法的实现基于这个理论,在创建 activity 时进行所有的初始化工作。
打开 Activity
现在,app 除了一堆无用的 0 和 1 以外没有任何用途,因为你根本不能向待办列表中添加任何东西。你需要改变这种状况,这就是你接下来的工作。
在打开的 MainActivity 文件中,在类顶部加入:
private final int ADD_TASK_REQUEST = 1;
这个变量用于记录你后面要打开的 intent。可以向它赋予任何 int 值。如果 activity 返回时所带的值等于你请求时指定的值,则你可以认为你的请求被成功处理了。这等会再讨论。
在 addTaskClick 方法中添加:
Intent intent = new Intent(MainActivity.this, TaskDescriptionActivity.class);
startActivityForResult(intent, ADD_TASK_REQUEST);
当你点击 Add a Task 按钮,会调用 addTaskClicked 方法。
这里我们创建了一个 Intent 以在 MainActivity 中打开 TaskDescriptionActivity。它需要从 TaskDescriptionActivity 返回一个结果,因为 app 需要知道是否需要添加一个新的待办到列表中。
要打开一个 activity 需要用到 startActivityForResult(…) 方法。
注意:如果不需要返回结果,可以调用 startActivity(…) 方法。
当 TaskDescriptionActivity 关闭,它会返回一个结果,通过 intent 的 onActivityResult(…) 方法。在 MainActivity 底部添加这个方法实现:
一切看起来都很好,真的吗?
并不完全这样。Forget Me Not 需要跳到另一个 activity 以显示待办的具体描述,因此接下来需要创建这个 activity。
创建 Activity
用 Android Studio 创建一个 activity 非常简单。只需要在想添加 activity 的包名上——也就是 com.raywenderlish.todolist 上——右击,然后选择 New\Activity,再选择 Empty Activity 这个最基本的 activity 模板。
在第二个窗口中,输入 Activity 名称,Android Studio 会自动填充其它字段。我们将Activity 命名为 TaskDescriptionActivity。
点击 Finish,举起双手庆贺吧!你已经创建了你的第一个 activity。
Android Studio 会自动生成创建该 activity 所需的资源,包括:
- 类: 一个名为 TaskDescriptionActivity.java 的类文件,文件在对应的 Java 包下面。在类中你可以实现 Activity 的功能。它必须是 Activity 子类或者孙子类。
- 布局: 一个名为 activity_task_description.xml 的布局文件,位于 res/layout 文件夹下。它定义了 activity 打开时每个 UI 元素在屏幕上的位置和大小。
此外,在 AndroidManifest.xml 文件中还添加了一行:
<activity
<activity android:name=".TaskDescriptionActivity" >
</activity>
这个 标签定义了该 activity。Android app 害有一种强迫症,所有要用的 activity 都必须在 manifest 文件中进行声明,以确保 app 只拥有声明在这里的 activity。
你肯定不愿意你的 app 突然调用了错误的 activity 或者更老火的是,它所用的 activity 会被其它 app 在未经许可的情况下使用。
这个标签中有几个属性,比如 label 和 icon,或者用于指定 activity UI 样式的 theme 属性。
仅有 android:name 属性是必须的。用于指定 activity 的类名。
运行程序,当你点击 Add a Task,新建的 activity 呈现。
看起来不错,但 activity 还缺少内容。赶紧的,来搞定它!
在 TaskDescriptionActivity 中,粘贴下列内容到类定义中:
public static final String EXTRA_TASK_DESCRIPTION = "task";
private EditText mDescriptionView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_description);
mDescriptionView = (EditText) findViewById(R.id.descriptionText);
}
public void doneClicked(View view) {
}
打开 layout/activity_task_description 将内容替换为:
<RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.raywenderlich.todolist.TaskDescriptionActivity">
<TextView
android:id="@+id/descriptionLabel"
android:text="@string/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:padding="20dp"/>
<EditText
android:id="@+id/descriptionText"
android:layout_width="match_parent"
android:layout_height="100dp"
android:padding="20dp"
android:layout_below="@+id/descriptionLabel"/>
<Button
android:id="@+id/doneBtn"
android:text="@string/done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/descriptionText"
android:padding="20dp"
android:onClick="doneClicked"/>
</RelativeLayout>
再次运行程序,点击 Add a Task,你的新 activity 终于有点东西可看了:
关闭 Activity
正确关闭 activity 和正确打开它同样重要。
在 TaskDescriptionActivity 中,添加下列代码到 doneClicked, 这个方法在 Done 按钮被点击后调用:
// 1
String taskDescription = mDescriptionView.getText().toString();
if (!taskDescription.isEmpty()) {
// 2
Intent result = new Intent();
result.putExtra(EXTRA_TASK_DESCRIPTION, taskDescription);
setResult(RESULT_OK, result);
} else {
// 3
setResult(RESULT_CANCELED);
}
// 4
finish();
这段代码做了这些事情:
- 从 TextView 中获取待办的描述。
- 创建了一个用于返回 MainActivity 的 intent,如果上一步获取的描述不为空的话。然后将待办描述封装到 Intent 中,并将返回结果标记为 RESULT_OK,以表示用户输入了一个有效的待办。
- 设置 result 为 RESULT_CANCELED,表示用户没有输入任何待办,我们从第一步中得到的任务描述是空。
- 关闭 activity。
当调用 finish() 时,会调用 MainActivity 中的 onActivityResult(…) 方法,导致想列表中添加该条待办。
这样,你就需要在 MainActivity 底部添加一个方法:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 1 - 判断你需要处理的请求是否正确
if (requestCode == ADD_TASK_REQUEST) {
// 2 - 判断请求是否成功返回
if (resultCode == RESULT_OK) {
// 3 - 用户输入了有效的待办。将任务添加到任务列表。
String task = data.getStringExtra(TaskDescriptionActivity.EXTRA_TASK_DESCRIPTION);
mList.add(task);
// 4
mAdapter.notifyDataSetChanged();
}
}
}
代码解释如下:
- 判断 requestCode ,以保证返回结果就是我们需要的——即我们所打开的 TaskDescriptionActivity 所返回的。
- 我们确认了 resultCode 是 RESULT_OK — 这个是标准的 activity 用于表示操作成功的返回结果。
- 这里从返回的 intent 中检索出任务描述,然后将它添加到 mList 数组。
- 最后,调用列表监听器的 notifyDataSetChanged() 方法。这会通知 listView 数据模型已经改变,它会刷新视图。
运行程序,app 启动后点击 Add a Task 按钮。这会打开一个新的窗口让你输入待办任务。添加任务描述,然后点击 Done。窗口关闭,新任务会列在待办列表中:
实现回调方法
每个 todo 列表都需要对时间进行控制,这应当是你接下来应当做的事情。打开 MainActivity 在原有 member 变量后添加:
private BroadcastReceiver mTickReceiver;
在 onCreate() 方法中添加如下代码,初始化一个 BroadcastReceiver 实例:
mTickReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_TIME_TICK)) {
mDateTimeTextView.setText(getCurrentTimeStamp());
}
}
};
这里,我们创建了一个 BroadcastReceiver,当我们从系统接收到时间变化通知时修改屏幕上的时间和日期。我们使用了 getCurretnTimeStamp () 方法,这是 activity 的一个助手方法,用于返回当前日期和时间。
注意:如果你不熟悉 BroadcastReceivers,请参考Android 开发者文档。
然后,在 MainActivity 的 onCreate() 方法下面添加:
@Override
protected void onResume() {
// 1
super.onResume();
// 2
mDateTimeTextView.setText(getCurrentTimeStamp());
// 3
registerReceiver(mTickReceiver, new IntentFilter(Intent.ACTION_TIME_TICK));
}
@Override
protected void onPause() {
// 4
super.onPause();
// 5
if (mTickReceiver != null) {
try {
unregisterReceiver(mTickReceiver);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Timetick Receiver not registered", e);
}
}
}
代码解释如下:
- 调用父类的 onResume()。
- 将 timeTextView 修改为当前值。
- 在 onResume() 中注册广播接收器。以便接受类型为 ACTION_TIME_TICK 的广播。每当时间发生改变,就会发送这类广播。
- 调用父类的 onPause()。
- 在 onPause() 中注销广播接收器,这样 app 就不再收听时间变化广播。这能节省不必要的系统开销。
运行程序。现在能够看到当前日期时间。如果你跳到添加待办界面,然后返回,这个时间仍然能够实时刷新。
状态持久化
每个 todo 列表都会对你需要干的事情有个好记性,除了你的好朋友 Forget Me Not 以外。很不幸,这个 app 真的十分健忘。你可以试试看。
打开程序,执行下列步骤。
- 点击 Add a Task。
- 输入 “Replace regular with decaf in the breakroom” ,然后按 Done。你可以在待办列表中看到这个任务。
- 从“最近 app 列表”中关掉 app。
- 重新打开 app。
你会发现,它把你的邪恶计划给忘掉了!
在两次运行间存储数据
打开 MainActivity,在类顶部增加下列变量:
private final String PREFS_TASKS = "prefs_tasks";
private final String KEY_TASKS_LIST = "list";
在其它 activity 生命周期方法下增加如下方法:
@Override
protected void onStop() {
super.onStop();
// 保存数据
StringBuilder savedList = new StringBuilder();
for (String s : mList) {
savedList.append(s);
savedList.append(",");
}
getSharedPreferences(PREFS_TASKS, MODE_PRIVATE).edit()
.putString(KEY_TASKS_LIST, savedList.toString()).commit();
}
在 onStop 方法中,你用待办列表中的任务描述创建了一个以逗号分隔的字符串,然后将字符串保存到 sharedPreferences 中。前面说过,在这个方法中提交任何未保存的修改是一个最佳体验。
在 onCreate() 方法中,在 mList 的初始化代码之后加入:
String savedList = getSharedPreferences(PREFS_TASKS, MODE_PRIVATE).getString(KEY_TASKS_LIST, null);
if (savedList != null) {
String[] items = savedList.split(",");
mList = new ArrayList<String>(Arrays.asList(items));
}
这里,我们从 SharedPreferences 读取已保存的任务列表,然后将这个逗号分隔的字符串转换为 ArrayList 然后赋给 mList。
注意:因为你只需要保存原始数据类型,所以这里使用 SharedPreferences 就行了。如果是复杂数据,你可以用其他 Android 上有效的存储方案。
运行程序。添加任务,关闭再打开 app。发现有啥不同没?你能够将任务“保持”在待办列表中!
设置修改
你需要能够删除待办列表中的内容。
打开 MainActivity,在类的底部加入:
private void taskSelected(final int position) {
// 1
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(MainActivity.this);
// 2
alertDialogBuilder.setTitle(R.string.alert_title);
// 3
alertDialogBuilder
.setMessage(mList.get(position))
.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
mList.remove(position);
mAdapter.notifyDataSetChanged();
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
// 4
AlertDialog alertDialog = alertDialogBuilder.create();
// 5
alertDialog.show();
}
简单说,我们创建了一个 alert 并在用户选中一个任务时弹出一个对话框。代码解释如下:
- 创建一个 AlertDialog.Builder,用于创建一个 AlertDialog。
- 设置对话框的标题。
- 设置对话框的消息内容为所选任务描述。然后实现了一个 PositiveButton,用它来删除任务并刷新待办列表;以及实现了一个 NegativeButton,用于解散对话框。
- 用 AlertDialog.Builder 对象创建一个 alert 对话框。
- 显示对话框。
在 onCreate() 方法,找到 listView 的 OnItemClickListener 的 onItemClick(…) 方法中加入:
taskSelected(i);
将 app 中的字符串和代码进行分离是一种良好体验。理由是你可以轻易修改它,尤其是当字符串在多个地方被用到的时候。在将 app 翻译成其它语言时也很方便。
打开 values/strings.xml 在 resources 标签中添加:
<string name="alert_title">Task</string>
<string name="delete">Delete</string>
<string name="cancel">Cancel</string>
运行程序,点击某条待办。你会看到一个对话框,带有 CANCEL 按钮和 DELETE 按钮:
打开 app 点击一条任务,打开对话框。然后旋转设备,确保你的设备设置中的旋屏选项设置为“自动”。
当你旋转设备时,对话框会消失。对于用户体验来说,这真的不可靠和非常令人讨厌。用户不会喜欢这种事情发生,平白无故的东西就从屏幕上消失了,因此你需要让用户手动才能解散对话框。
处理配置变化
配置变化,比如旋屏,键盘显示等,会导致 activity 关闭和重启。你可以在这里找到完整的会导致 activity 重启的系统事件列表。
有几个用于处理配置变化的方法。其中一个是在 AndroidManifest.xml 中,MainActivity 的 activity 元素的 android:name 后面添加这句:
android:configChanges="orientation|screenSize">
这里,你声明 MainActivity 会处理关于方向和屏幕尺寸变化的配置变化。这能够防止你的 activity 被系统重启,系统会将控制传递给 MainActivity。
为了让 MainActivity 处理配置变化,有几个地方需要修改。在类底部添加一个方法:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
这里仅仅需要调用父类的 onConfigurationChanged() 方法,因为我们不需要在旋屏时改变任何元素。
onConfigurationChanged() 传入一个 configuration 对象,这个对象包含了改变后的设备配置信息。
你可以对新的 configuration 对象进行检查,读取 configuration 的字段,进行适当的修改界面上所用的资源。
现在,运行程序。重复之前说的第 1 步和第 2 步。这次,对话框不会消失,知道你解散它。
另外一个可替换的做法是,持有一个状态对象,将它传递给 activity 的重建的实例。你可以实现 onSaveInstanceState() 回调来达到这个目的。
如果用这种方法,系统将你的 activity 状态保存在一个 bundle 中,你可以在对应的 onRestoreInstanceState() 回调中恢复它。但是,这个 bundle 不是用于大数据量(比如位图)存储的,因此只能存储可串行化的数据。
第二种办法的缺点是,在配置变化发生时进行串行化和反串行化数据会导致较大的性能开销。它会消耗大量内存,并降低 activity 的重启速度。
在这种情况下,用一个 fragment 去处理配置变化是最可行的方法。在我们的Android Fragements 教程 中,会介绍 fragment 以及如何用它保存 activity 重启时的数据。
结束
恭喜!你已经学习了 Android 中的 activity 的基本用法,对 activity 生命周期的核心概念有了极好的理解。
你学习了几个概念,包括:
- 如何创建 activity
- 如何关闭 activity
- 当 activity 关闭时如何保存数据
- 如何处理配置改变
你可以从这里下载完成好的项目。如果你还想学习更多内容,请阅读Google 的文档。
我希望你喜欢本教程,如果有任何问题、建议或对 demo 项目有任何改进建议,请在下面留言!