# 第二十一个项目——使用Loader实现异步数据加载操作

## 实验目的

* 掌握**Loader**框架的基本原理；
* 掌握**AsyncTaskLoader**、**CursorLoader**及**LoaderManager**等组件的使用方法；

## 实验要求

* 掌握使用**Loader**进行异步数据加载的方法；

## 实验内容

在[第九个项目](https://xxgqin.gitbook.io/android/ch04/ch04-2.md)中，我们实现了通过读取**SQLite**数据库加载新闻列表，并为了简化读取数据库的操作定义了**NewsCursorAdapter**适配器，用于直接将数据库查询得到的**Cursor**游标对象作为适配器的数据源。

为了简便起见，这类数据读取操作放在**MainActivity**的活动组件中。然而实际项目开发中，无论数据是从数据库或是 云端服务器进行读取，这类数据操作通常会比较耗时，这势必会占用**UI**主线程资源，从而影响**App**与用户的交互体验。

如果通过**AsyncTask**异步活动在子线程中进行数据读取，则开发人员还需要在**Activity**或**Fragment**生命周期中 管理主线程、子线程的状态，这增加了应用开发的复杂度。

本实验中使用**Android**提供的**Loader**异步加载框架解决上述问题，将数据读取等操作放入**Loader**框架中进行管理， 又可降低在**Activity**或**Fragment**中管理数据读取子线程状态的复杂度。

在**Loader**异步加载框架中主要需要熟悉两个组件的使用方法，其一是**LoaderManager**，它用于在**Activity**或**Fragement**中管理多个**Loader**实例，每个**Loader**实例可对应一个**AsyncTaskLoader**对象；其二是**AsyncTaskLoader**，它在**AsyncTask**的基础上做了封装，每一个**AsyncTaskLoader**实例会启动一个**AsyncTask**，从而启动一个子线程执行数据异步操作。

### 步骤一，打开第九个项目

打开[第九个项目](https://xxgqin.gitbook.io/android/ch04/ch04-2.md)，在该项目基础上完成本次实验。

### 步骤二，定义NewsQueryCursorLoader类

在**Android Studio**中新建一个名为**NewsQueryCursorLoader**的Java类，该类继承至**CursorLoader** （**CursorLoader**继承至**AsyncTaskLoader**)。在该类中，有两个方法需要进行重写，分别是 **onStartLoading()**&#x53CA;**loadInBackground()**&#x65B9;法，其中**onStartLoading()**&#x65B9;法调用了**forceLoad()**&#x65B9;法强制**NewsQueryAsyncCursorLoader**类实例重新执行**loadInBackground()**&#x65B9;法；

而**loadInBackground()**&#x65B9;法则是实际放入子线程执行的代码，在该方法中把数据库的查询操作代码放入其中，而该方法的返回值为**Cursor**类型，则是从子线程中返回的数据。**loadInBackground()**&#x65B9;法的返回值将作为**LoaderManager.LoaderCallbacks**接口的**onLoadFinished()**&#x65B9;法的参数回传给调用**LoaderManager**的组件。

以下为**NewsQueryAsyncCursorLoader**类定义：

```java
import android.content.Context;
import android.content.CursorLoader;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

public class NewsQueryAsyncCursorLoader extends CursorLoader {
    private Context mContext;

    public NewsQueryAsyncCursorLoader(Context context) {
        super(context);
        mContext = context;
    }

    @Override
    protected void onStartLoading() {
        forceLoad();
    }

    @Override
    public Cursor loadInBackground() {
        MyDbOpenHelper dbOpenHelper = new MyDbOpenHelper(mContext);
        SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
        Cursor cursor = db.query(
                NewsContract.NewsEntry.TABLE_NAME,
                null,
                null,
                null,
                null,
                null,
                NewsContract.NewsEntry._ID +" DESC");

        return cursor;
    }
}
```

### 步骤三，在MainActivity中实现LoaderManager.LoaderCallbacks接口

在**Activity**或**Fragement**中使用**LoaderManager**需要做的工作主要包括两部分：

* 实现**LoaderManager.LoaderCallbacks**接口，该接口需要实现**onCreateLoader()**、**onLoaderFinished()**、**onLoaderReset()**&#x65B9;法；分别用于当**Loader**实例被创建、完成、重置时执行的回调方法；
* 获取**LoaderManager**实例并调用其**initLoader()**&#x6216;**restartLoader()**&#x65B9;法启动**Loader**实例；

通过在**MainActivity**中实现**LoaderManager.LoaderCallbacks**接口，该接口需要一个泛型参数，其指明接口中的三个方法返回值或参数类型：

* Loader onCreateLoader(int i, Bundle bundle);
* void onLoadFinished(Loader loader, D obj);
* void onLoaderReset(Loader loader);

**1. 实现LoaderManager.LoaderCallbacks接口**

由于本实验中，数据从数据库进行查询，其返回值类型为**Cursor**，因此指明**LoaderManager.LoaderCallbacks**泛型参数为**Cursor**，具体代码见本文最后一段代码。

接下来逐一讲解**LoaderManager.LoaderCallbacks**接口的三个重写方法。

**首先**，重写**Loader onCreateLoader(int i, Bundle bundle)**&#x65B9;法，该方法的返回值为**Loader**类型，而第一个参&#x6570;_&#x69;_&#x8868;示的是**Loader**的编号，通常由**LoaderManager**的**initLoader()**&#x6216;**restartLoader()**&#x65B9;法指定，开发者可根据该参数决定实例化哪个**Loader**类实例（实际开发中，可能需要多个**Loader**实例对数据进行不同操作，例如查询、更新、插入、删除等操作都可能由不同的自定义**Loader**子类完成；具体而言，本实验中**NewsQueryAsyncCursorLoader**实现对数据的查询操作，而插入操作则需要**NewsInsertAsyncCursorLoader**来完成）；第二个参数是传递给**Loader**的**Bundle**类型值，在本实验中并未使用到。 **onCreateLoader()**&#x65B9;法在**LoaderManager**调用**initLoader()**&#x6216;**restartLoader()**&#x65B9;法后被回调执行。

可以看到在**MainActivity**类中，为了简便起见，直接返回了一个匿名**NewsQueryAsyncCursorLoader**类对象。实际开发中通常需要通过**switch**或**if**语句判断参&#x6570;_&#x69;_&#x7684;值，从而实例化不同的**Loader**子类。

**其次**，重写**void onLoadFinished(Loader loader, Cursor cursor)**&#x65B9;法，该方法第一个参数为在**onCreateLoader()**&#x51FD;数返回的**Loader**（实际为**NewsQueryAsyncCursorLoader**）实例，第二个参数为**Loader**实例执行完成**loadInBackground()**&#x51FD;数后的返回值**Cursor**对象（也就是**NewsQueryAsyncCursorLoader**中查询到的数据库结果集**Cursor**对象）。该方法在对应的**Loader**实例的**loadInBackground()**&#x65B9;法执行完成后被回调执行。

在该方法中，通过**cursorAdapter.swapCursor()**&#x65B9;法切换了最新的数据库查询结果集**Cursor**对象，并调用**notifyDataSetChanged()**&#x65B9;法通知**ListView**刷新列表数据。

**最后**，重写**void onLoaderReset(Loader loader)**&#x65B9;法。该方法在对应的**Loader**被重置时或**LoaderManager**调用**destroyLoader()**&#x65B9;法或**Activity**、**Fragement**被销毁时被回调执行。通常需要在该方法内将引用到某一个**Loader**数据的对象进行删除。

**2. 调用LoaderManager.initLoader()方法启动Loader**

通过调用**LoaderManager**对象调用**initLoader()**&#x65B9;法启动**Loader**从而实现数据的异步读取操作，该方法接受3个参数，分别是：

* _int i_，表示要初始化的**Loader**编号，开发人员可自定义每个**Loader**对应的整型编号，从而在**onStartLoading()**、**onLoadFinished()**&#x7B49;方法内进行判断执行相应的业务代码；
* _Bundle bundle_，表示传递给**onStartLoading()**&#x65B9;法的**Bundle**类型参数，通常用于初始化**Loader**，具体视**Loader**子类实现而异；
* _Context context_，表示上下文参数；

在**Activity**、**Fragement**等组件中，通过**getLoaderManager()**&#x65B9;法可获得**LoaderManager**实例。在**MainActivity**活动的**onCreate()**&#x65B9;法中，当**ListView**、**NewsCursorAdapter**等对象实例化完成后，调用了**getLoaderManager()**&#x65B9;法获取了**LoaderManager**实例，接着调用**initLoader()**&#x65B9;法初始化**Loader**实现新闻列表数据的异步读取操作。

以下为修改后的**MainActivity**类代码：

```java
 public class MainActivity extends AppCompatActivity 
         implements LoaderManager.LoaderCallbacks<Cursor> {
    ...
    private CursorAdapter cursorAdapter =null;
    private ListView lvNewsList;
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...

        lvNewsList = findViewById(R.id.lv_news_list);
        myDbHelper = new MyDbOpenHelper(MainActivity.this);
        db = myDbHelper.getWritableDatabase();

        cursorAdapter = new NewsCursorAdapter(MainActivity.this);
        cursorAdapter.swapCursor(null);

        lvNewsList.setAdapter(cursorAdapter);
        getLoaderManager().initLoader(0, null, this);
        ...
    }

    @Override
    public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
        return new NewsQueryAsyncCursorLoader(MainActivity.this);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        cursorAdapter.swapCursor(cursor);
        cursorAdapter.notifyDataSetChanged();
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
    }
}
```

### 步骤五，编译并运行App

编译本项目，成功后在**AVD**上或物理机上运行**App**，其运行效果与[第九个项目](https://xxgqin.gitbook.io/android/ch04/ch04-2.md)相同。

{% hint style="info" %}
请思考如何结合**Loader**框架与**SwipeRefreshLayout**控件实现新闻列表的异步下拉刷新。
{% endhint %}

## 实验小结

通过本次实验，你应该掌握了如下知识内容：

* 使用**AsyncTaskLoader**、**CursorLoader**及**LoaderManager**等组件进行异步数据加载；
