4.10使用AsyncTask做后台处理
问题
您必须进行一些繁重的处理,或从网络加载资源,并且您想在UI中显示进度和结果。
解
使用AsyncTask和ProgressDialog。
讨论
正如Android Dev Guide的“进程和线程”部分所述,您不应该阻止UI线程,也不应该从UI线程之外访问Android UI工具包。坏事情会发生,如果你这样做。
你可以在后台运行进程并在UI线程(a.k.a.主线程)中以多种方式更新UI,但使用AsyncTask类非常方便,在每个Android开发人员都应该知道如何做。
步骤归结为创建一个扩展AsyncTask的类。 AsyncTask本身是抽象的,并有一个抽象方法,Result doInBackground(Params ... params); AsyncTask只是创建一个可调用的工作线程,其中运行doInBackground的实现。结果和参数是我们需要在类定义中定义的两种类型。第三个是Progress类型,我们将在后面讨论。
我们的第一个实现将在后台执行所有操作,在标题栏中显示用户一个微调框,并在处理完成后更新ListView。这是典型的用例,不干扰用户手边的任务,并在检索结果时更新UI。
第二个实现将使用模态对话框来显示在后台进行的处理。在某些情况下,我们希望防止用户在发生某些处理时做其他事情,这是一个很好的方法。
我们将创建一个包含三个Button和一个Listview的UI。第一个按钮是开始我们的第一个刷新过程。第二个是用于其他刷新过程,第三个是清除ListView中的结果(请参阅示例4-16)。
实例4-16。主要布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button android:text="Refresh 1" android:id="@+id/button1"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:layout_weight="1"></Button>
<Button android:text="Refresh 2" android:id="@+id/button2"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:layout_weight="1"></Button>
<Button android:text="Clear" android:id="@+id/button3"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:layout_weight="1"></Button>
</LinearLayout>
<ListView android:id="@+id/listView1" android:layout_height="fill_parent"
android:layout_width="fill_parent"></ListView>
</LinearLayout>
我们将这些UI元素分配给onCreate中的各个字段,并添加一些点击监听器(参见示例4-17)。
实例4-17。 onCreate()和onItemClick()方法
ListView mListView;
Button mClear;
Button mRefresh1;
Button mRefresh2;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mListView = (ListView) findViewById(R.id.listView1);
mListView.setTextFilterEnabled(true);
mListView.setOnItemClickListener(this);
mRefresh1 = (Button) findViewById(R.id.button1);
mClear = (Button) findViewById(R.id.button3);
mClear.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListView.setAdapter(null);
}
});
}
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Datum datum = (Datum) mListView.getItemAtPosition(position);
Uri uri = Uri.parse("http://androidcookbook.com/Recipe.seam?recipeId=" +
datum.getId());
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
this.startActivity(intent);
}
以下小节描述两种使用情况:在后台处理和在前台处理。
用例1:在后台处理
首先我们创建一个扩展AsyncTask的内部类:
protected class LoadRecipesTask1 extends AsyncTask<String, Void, ArrayList<Datum>> {
...
}
如你所见,我们必须为类定义提供三种类型。第一种是在启动此后台任务时提供的参数类型,在我们的例子中是一个包含URL的String。第二种类型用于进度更新(我们将在后面使用)。第三种类型是由doInBackground方法的实现返回的类型,通常是可以更新特定UI元素(在我们的例子中是ListView)的东西。
让我们实现doInBackground方法:
@Override
protected ArrayList<Datum> doInBackground(String... urls) {
ArrayList<Datum> datumList = new ArrayList<Datum>();
try {
datumList = parse(urls[0]);
} catch (IOException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
e.printStackTrace();
}
return datumList;
}
正如你所看到的,这很简单。解析方法(创建一个Datum对象列表)未显示,因为它与一个应用程序中的特定数据格式相关。然后,doInBackground方法的结果作为参数传递给同一(内部)类中的onPostExecute方法。在这种方法中,我们允许更新布局中的UI元素,所以我们设置ListView的适配器来显示我们的结果。
@Override
protected void onPostExecute(ArrayList<Datum> result) {
mListView.setAdapter(new ArrayAdapter<Datum>(MainActivity.this,
R.layout.list_item, result));
}
现在我们需要一种方法来启动这个任务。我们在mRefresh1的onClickListener中通过调用AsyncTask的execute(Params ... params)方法(在我们的例子中是execute(String ... urls))来做到这一点。
mRefresh1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
LoadRecipesTask1 mLoadRecipesTask = new LoadRecipesTask1();
mLoadRecipesTask.execute(
"http://androidcookbook.com/seam/resource/rest/recipe/list");
}
});
现在,当您启动应用程序时,它确实检索配方并填充ListView,但用户不知道在后台发生了什么。为了通知用户锻造活动,我们可以将窗口状态设置为“不确定进度”;这会在应用程序标题栏的右上方显示一个小进度动画。我们通过调用onCreate中的以下方法请求此功能:
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 。
然后我们可以通过在内部类的一个新方法onPreExecute中调用setProgressBarIndeterminateVisibility(Boolean visibility)方法来启动进度动画。
protected void onPreExecute() {
MainActivity.this.setProgressBarIndeterminateVisibility(true);
}
我们通过在onPostExecute方法中调用相同的方法来停止我们窗口标题中的旋转进度条,它将变为:
protected void onPostExecute(ArrayList<Datum> result) {
mListView.setAdapter(new ArrayAdapter<Datum>(MainActivity.this,
R.layout.list_item, result));
MainActivity.this.setProgressBarIndeterminateVisibility(false);
}
我们完成了!带你的应用程序的旋转(双关意)。
您可以看到,这是一个漂亮的功能,用于创建更好的用户体验!
用例2:在前台处理
在本例中,我们向用户显示一个模态对话框,显示在后台加载食谱的进度。这样的对话框称为ProgressDialog。首先,我们将它作为字段添加到我们的活动。
ProgressDialog mProgressDialog;
然后我们添加onCreateDialog方法,以便能够回答showDialog调用并创建我们的对话框。
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_KEY:
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.setMessage("Retrieving recipes...");
mProgressDialog.setCancelable(false);
return mProgressDialog;
}
return null;
}
1. 我们应该在这里处理请求和创建所有对话框。 DIALOG_KEY是一个int常量,具有任意值(我们使用0)来标识此对话框。
2.我们将进度风格设置为STYLE_HORIZONTAL,它显示一个水平进度条。默认值为STYLE_SPINNER。
3.我们设置我们的自定义消息,它显示在进度条上面。
4.通过调用setCancelable参数false,我们禁用后退按钮,使此对话框模式。
我们的AsyncTask的新实现如例4-18所示。
实例4-18。 AsyncTask实现
protected class LoadRecipesTask2 extends AsyncTask<String, Integer, ArrayList<Datum>>{
@Override
protected void onPreExecute() {
mProgressDialog.show();
}
@Override
protected ArrayList<Datum> doInBackground(String... urls) {
ArrayList<Datum> datumList = new ArrayList<Datum>();
for (int i = 0; i < urls.length; i++) {
try {
datumList = parse(urls[i]);
publishProgress((int) (((i+1) / (float) urls.length) * 100));
} catch (IOException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
e.printStackTrace();
}
}
return datumList;
}
@Override
protected void onProgressUpdate(Integer... values) {
mProgressDialog.setProgress(values[0]);
}
@Override
protected void onPostExecute(ArrayList<Datum> result) {
mListView.setAdapter(new ArrayAdapter<Datum>(
MainActivity.this, R.layout.list_item, result));
mProgressDialog.dismiss();
}
}
我们在这里看到一些新的东西。
在我们开始我们的后台进程之前,我们显示模态对话框。
2.在我们的后台进程中,我们遍历所有的URL,希望接收多个URL。这将给我们一个很好的指示我们的进步。
我们可以通过调用publishProgress来更新进度。请注意,参数是类型int,它将被自动装入到类定义中定义的第二个类型Integer。
4.对publishProgress的调用将导致对onProgressUpdate的调用,该调用再次具有类型为Integer的参数。你可以,当然,使用String或其他作为参数类型,只需将内部类定义中的第二个类型更改为String,当然,在调用publishProgress。
5.我们使用第一个Integer在ProgressDialog中设置新的进度值。
6.我们关闭对话框,删除它。
现在我们可以通过实现我们的第二个刷新按钮的onClickListener来绑定所有这些。
mRefresh2.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
LoadRecipesTask2 mLoadRecipesTask = new LoadRecipesTask2();
String url =
"http://androidcookbook.com/seam/resource/rest/recipe/list";
showDialog(DIALOG_KEY);
mLoadRecipesTask.execute(url, url, url, url, url);
}
});
我们通过使用DIALOG_KEY参数调用showDialog来显示对话框,这将触发我们以前定义的onCreateDialog方法。
2.我们用五个URL执行新任务,只是为了显示一点进度。
它看起来像图4-1。
图4-1。 在后台检索
秘诀
结论
使用AsyncTask实现后台任务非常简单,应该对所有需要更新用户界面的长时间运行的进程执行。