Android异步加载数据伴侣,TaskLoader与LoaderManager

1 前面的话

App大部分的业务场景是这样:从服务端拉取数据,返回结果后进行展示。Android中,每一个界面都是由Activity或者Fragment托管的,通常情况下,业务的逻辑和生命周期强耦合。很多时候,我们需要决定什么时候拉取数据,什么时候使用缓存数据。例如:

以上两种情况,拉取数据的策略是不同的。我们希望在首次进入页面的时候进行数据拉取,但是当配置发生变化时,直接使用上次请求得到的数据。那么,有没有一种较为优雅的解决方案?有的,那就是Loaders

2 认识Loaders

使用Loaders可以大大简化Activity或者Fragment加载异步数据。引用官方一句话:

Loaders make it easy to asynchronously load data in an activity or fragment.

谈到异步任务,最基础的莫过于Handler了。但是Handler在实际过程中较为复杂,编写的代码可读性也较差。Android为了降低异步编程的复杂度,引入了AsyncTask。Loaders的设计目的也是为了简化数据的异步操作,并且,在使用上和AsyncTask十分相似。它具有如下特点:

  • They are available to every Activity and Fragment.(就地取材)
  • They provide asynchronous loading of data.(用途)
  • They monitor the source of their data and deliver new results when the content changes.(感知数据变化)
  • They automatically reconnect to the last loader’s cursor when being recreated after a configuration change. Thus, they don’t need to re-query their data.

Loaders的强大之处在于后面2点。如果对每个数据源编写对应Observer,当数据源发生变化时,可以通知Loaders重新加载数据;手机发生转屏导致Activity或者Fragment重建,Loaders可以直接使用上次请求的结果,而不需要再次请求。

3 使用它

使用Loaders做数据异步加载,包括两部分:指定TaskLoader,使用LoaderManager管理TaskLoader。

3.1 定义TaskLoader

官方例子是读取手机通讯录,这里我们使用一种更通用的场景,网络请求。当然,TaskLoader可以是任何你需要异步的操作集合。Api使用豆瓣电影,定义一个InTheatersTaskLoader,用于请求正在热映,相关代码如下:

TaskLoaderResult

保存异步任务结果,包括数据和异常信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TaskLoaderResult<T> {

private Exception mException;
private T mData;

public TaskLoaderResult() {
}

public TaskLoaderResult(Exception exception, T data) {
mException = exception;
mData = data;
}

public Exception getException() {
return mException;
}

public void setException(Exception exception) {
mException = exception;
}

public T getData() {
return mData;
}

public void setData(T data) {
mData = data;
}
}

InTheaters

Json对象,用于Gson序列化/反序列化。这里使用推荐Android Studio神级插件GsonFormat进行自动生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TaskLoaderResult<T> {

private Exception mException;
private T mData;

public TaskLoaderResult() {
}

public TaskLoaderResult(Exception exception, T data) {
mException = exception;
mData = data;
}

public Exception getException() {
return mException;
}

public void setException(Exception exception) {
mException = exception;
}

public T getData() {
return mData;
}

public void setData(T data) {
mData = data;
}
}

AsyncTaskLoader

所有的异步操作都应该放在这里,并负责保存结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class InTheatersTaskLoader extends AsyncTaskLoader<TaskLoaderResult<InTheaters>> {

private static final String TAG = InTheatersTaskLoader.class.getSimpleName();
private final Context mContext;
private TaskLoaderResult<InTheaters> mResult;

public InTheatersTaskLoader(Context context) {
super(context);

mContext = context;
}

@Override
protected void onStartLoading() {
Log.d(TAG, "onStartLoading, mResult " + mResult);
super.onStartLoading();

if (mResult != null && mResult.getData() != null) {
deliverResult(mResult);
} else {
forceLoad();
}
}

@Override
public TaskLoaderResult<InTheaters> loadInBackground() {
Log.d(TAG, "thread " + Thread.currentThread().getId());

TaskLoaderResult<InTheaters> result = new TaskLoaderResult<>();

ApiService api = ApiProvider.getInstance(mContext).getApiService();
Call<InTheaters> call = api.getInTheaters("福州");
try {
result.setData(call.execute().body());
} catch (IOException e) {
e.printStackTrace();

result.setException(e);
}

return result;
}

@Override
public void deliverResult(TaskLoaderResult<InTheaters> data) {
Log.d(TAG, "deliverResult");
mResult = data;

super.deliverResult(data);
}
}

这里,我们就定义好了一个异步加载任务,用来从豆瓣拉取最新正在热映的电影。关于Api部分的封装,使用的是Retrofit + OkHttp组合,这里就不列出代码了。

3.2 使用LoaderManager管理Loader

定义好LoaderTask之后,我们还需要让外部启动它,才能完成数据加载。Android在Activity/Fragment中已经封装好了一个LoaderManager,直接调用getLoaderManager(),如果需要支持v4包,则调用兼容方法getSupportLoaderManager()。此外,需要让Activity/Fragment实现LoaderManager.LoaderCallbacks。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<TaskLoaderResult<InTheaters>>, View.OnClickListener {

private static final String TAG = MainActivity.class.getSimpleName();

private ListView mListView;
private ArrayAdapter<String> mAdapter;
private List<String> mData;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

mData = new ArrayList<>();
mAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, android.R
.id.text1, mData);

mListView = (ListView) findViewById(R.id.list);
mListView.setAdapter(mAdapter);
mListView.setEmptyView(findViewById(R.id.empty_item));

getSupportLoaderManager().initLoader(0, null, this);

FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();

getSupportLoaderManager().restartLoader(0, null, MainActivity.this);
}
});

findViewById(R.id.button).setOnClickListener(this);
}

@Override
protected void onStop() {
Log.d(TAG, "onStop");
super.onStop();
}

@Override
protected void onDestroy() {
Log.d(TAG, "onDestroy");
super.onDestroy();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();

//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}

return super.onOptionsItemSelected(item);
}

@Override
public Loader<TaskLoaderResult<InTheaters>> onCreateLoader(int id, Bundle args) {
Log.d(TAG, "onCreateLoader, id " + id);

return new InTheatersTaskLoader(this);
}

@Override
public void onLoadFinished(Loader<TaskLoaderResult<InTheaters>> loader, TaskLoaderResult<InTheaters> data) {
if (data.getException() != null) {
Log.e(TAG, data.getException().getMessage());
return;
}

for(InTheaters.Subject subject : data.getData().getSubjects()) {
mData.add(subject.getTitle());
}

mAdapter.addAll(mData);
mAdapter.notifyDataSetChanged();
}

@Override
public void onLoaderReset(Loader<TaskLoaderResult<InTheaters>> loader) {
Log.d(TAG, "onLoaderReset");

mAdapter.clear();
mAdapter.notifyDataSetChanged();
}

@Override
public void onClick(View v) {
if (v.getId() == R.id.button) {
startActivity(new Intent(this, FooActivity.class));
}
}
}

4 总结

  • 多个LoaderTask

如果一个Activity有多个LoaderTask,由于LoaderTask的id和处理的数据类型都不同,则需要为每个LoaderTask定义一个回调。

  • 异常处理

网络请求都包含异常处理逻辑,这里采用的是单独封装一个TaskResult类。如果抛出以异常,将异常信息set进去,并且data至为null。这时候Activity/Fragment在onFinished()中需要对TaskResult进行判断,如果异常信息不为空,进入异常处理逻辑。

完整示例代码,请移步我的Github

参考