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

实验目的

  • 掌握Loader框架的基本原理;

  • 掌握AsyncTaskLoaderCursorLoaderLoaderManager等组件的使用方法;

实验要求

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

实验内容

第九个项目中,我们实现了通过读取SQLite数据库加载新闻列表,并为了简化读取数据库的操作定义了NewsCursorAdapter适配器,用于直接将数据库查询得到的Cursor游标对象作为适配器的数据源。

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

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

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

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

步骤一,打开第九个项目

打开第九个项目,在该项目基础上完成本次实验。

步骤二,定义NewsQueryCursorLoader类

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

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

以下为NewsQueryAsyncCursorLoader类定义:

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接口

ActivityFragement中使用LoaderManager需要做的工作主要包括两部分:

  • 实现LoaderManager.LoaderCallbacks接口,该接口需要实现onCreateLoader()onLoaderFinished()onLoaderReset()方法;分别用于当Loader实例被创建、完成、重置时执行的回调方法;

  • 获取LoaderManager实例并调用其initLoader()restartLoader()方法启动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)方法,该方法的返回值为Loader类型,而第一个参数i表示的是Loader的编号,通常由LoaderManagerinitLoader()restartLoader()方法指定,开发者可根据该参数决定实例化哪个Loader类实例(实际开发中,可能需要多个Loader实例对数据进行不同操作,例如查询、更新、插入、删除等操作都可能由不同的自定义Loader子类完成;具体而言,本实验中NewsQueryAsyncCursorLoader实现对数据的查询操作,而插入操作则需要NewsInsertAsyncCursorLoader来完成);第二个参数是传递给LoaderBundle类型值,在本实验中并未使用到。 onCreateLoader()方法在LoaderManager调用initLoader()restartLoader()方法后被回调执行。

可以看到在MainActivity类中,为了简便起见,直接返回了一个匿名NewsQueryAsyncCursorLoader类对象。实际开发中通常需要通过switchif语句判断参数i的值,从而实例化不同的Loader子类。

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

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

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

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

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

  • int i,表示要初始化的Loader编号,开发人员可自定义每个Loader对应的整型编号,从而在onStartLoading()onLoadFinished()等方法内进行判断执行相应的业务代码;

  • Bundle bundle,表示传递给onStartLoading()方法的Bundle类型参数,通常用于初始化Loader,具体视Loader子类实现而异;

  • Context context,表示上下文参数;

ActivityFragement等组件中,通过getLoaderManager()方法可获得LoaderManager实例。在MainActivity活动的onCreate()方法中,当ListViewNewsCursorAdapter等对象实例化完成后,调用了getLoaderManager()方法获取了LoaderManager实例,接着调用initLoader()方法初始化Loader实现新闻列表数据的异步读取操作。

以下为修改后的MainActivity类代码:

 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,其运行效果与第九个项目相同。

实验小结

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

  • 使用AsyncTaskLoaderCursorLoaderLoaderManager等组件进行异步数据加载;

Last updated

Was this helpful?