Loaders

翻译:Mark(武汉大学)QQ:765731535

从Android 3.0开始引进了loader(加载器)技术, 在activity或者fragment中,loaders可以把异步地加载数据变得更简单。Loaders具有以下特性:

  • 他们对于每一个ActivityFragment都是有效的。
  • 他们可以提供异步加载数据的能力。
  • 他们监视数据源,并当内容改变时传递当前最新的结果。
  • 当他们因为配置的改变而重新连接的时候,他们会自动地重连到上一个loader的游标。因此,他们不需要重新查询数据。

Loader API汇总


在应用中使用loaders可能会涉及到多个类和接口。他们被汇总到了下表中:

类/接口 描述
LoaderManager 一个关联到ActivityFragment的抽象类,用来管理一个或多个 Loader实例 这样可以帮助一个应用管理那些跟Activity 或者Fragment的生命周期联系到一起的长时间运行的操作;这样的最常见的使用就是 CursorLoader,然而应用可以自由地写自己的加载器来加载其他类型的数据。

每一个activity或者fragment都只有一个LoaderManager。但是一个 LoaderManager可以拥有多个加载器(loaders)。
LoaderManager.LoaderCallbacks 一个用来去为客户端和LoaderManager提供交互的回调接口。 举个例子,你可以使用onCreateLoader() 回调方法来创建一个新的loader(加载器)。
Loader 一个执行异步的数据加载的抽象类。这是加载器的基类。你可以使用典型的CursorLoader,但是也可以实现你自己的子类。 当loaders被激活的时候,它们应该见识数据源并且当内容改变的时候传递最新结果。
AsyncTaskLoader 提供一个AsyncTask来执行异步加载数据的抽象loader。
CursorLoader AsyncTaskLoader的一个子类,查询 ContentResolver然后返回一个Cursor。这个类用标准的方式实现了 Loader的协议以此来查询cursors, AsyncTaskLoader在后台线程中执行cursor查询所以它不会阻塞应用的UI。 使用loader是从ContentProvider异步加载数据的最好的方式, 相对于通过fragment或activity的API来执行查询

上表中的那些类和接口都是你将会用来在应用中实现loader的极其重要的组件。你不必在创建每一个loader的时候全部使用,但是你总是需要一个 LoaderManager的引用,用来初始化一个loader Loader类的实现,类似于CursorLoader。 下面的章节将会向您展示怎样在应用中使用这些类和接口。

在应用中使用Loader


这一节描述了怎样在Android应用中使用loaders。一个典型地使用了loaders的应用应该包含以下内容:

开始使用Loader

LoaderManager管理一个ActivityFragment范围内的一个或多个Loader实例。每一个Activity或者Fragment都只有一个 LoaderManager

你可以典型地初始化一个 Loader,可以在activity的 onCreate()方法,也可以在fragment的 onActivityCreated()方法中。你可以像下面一样做这件事:

//准备loader,无论是与一个已经存在的loader重连,
//还是新建一个。
getLoaderManager().initLoader(0, null, this);

initLoader()方法需要以下参数:

  • 一个可以标识loader的唯一的ID。本例中的ID是0。
  • 一个可选参数,当loader初始化时提供给它(在本例中是null)。
  • 一个LoaderManager.LoaderCallbacks的实现,将会被 LoaderManager调用,用来报告loader的时间。本例中,本地类实现了 LoaderManager.LoaderCallbacks借口,所以它传递了一个自身的引用 this

initLoader()方法调用确保了一个loader会被初始化以及激活 它有两种可能的后果:

无论在哪一种情况中,传入的LoaderManager.LoaderCallbacks 实现都会跟loader绑定在一起,它将会在loader状态改变时被调用。如果在本次调用时,调用者处于开始状态,并且所请求的loader已经存在并产生了数据,那么系统就会立马调用 onLoadFinished() (即在initLoader()过程中), 所以你必须为这种情况做好准备。更多关于这个回调方法的讨论,请参看 onLoadFinished

请注意,initLoader() 方法返回被创建的Loader,但是你不必保留它的引用, LoaderManager会自动管理loader的生命, LoaderManager 会在必要的时候启动和终止,以及维护loader的状态和它关联的内容。这就意味着,你几乎不用和loader进行直接交互 (寻求使用loader方法来调整loader行为的例子,请参看 LoaderThrottle实例)。 你最常用的手段是当特定事件发生时,使用LoaderManager.LoaderCallbacks方法来介入到加载过程中。 更多关于本话题的讨论,请参看Using the LoaderManager Callbacks

重启一个Loader

当你像上面展示的那样使用initLoader()的时候, 如果有的话,它会使用已存在的带有标识ID的loader。 如果没有,它会创建一个。但是有时你会想要丢掉旧的数据,开始新的过程。

为了丢弃旧的数据,你要使用restartLoader()。 例如,SearchView.OnQueryTextListener的实现在用户查询改变时重启了, loader需要被重启从而能够使用修改过的搜索过滤进行新的查询:

public boolean onQueryTextChanged(String newText) {
    //当action bar的搜索文本改变时调用。
    //更新搜索过滤器,然后重启loader用当前的过滤器
    //做一次新的查询。
    mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
    getLoaderManager().restartLoader(0, null, this);
    return true;
}

使用LoaderManager回调

LoaderManager.LoaderCallbacks是一个回调接口,它为客户端提供与 is a callback interface LoaderManager交互的能力。

Loaders,尤其是CursorLoader,大家都希望当它被停止以后仍然可以保持数据。 这样允许应用在activity或fragment的 onStop()onStart()方法之间保持数据, 以至于当用户返回到一个应用的时候,他们不必再等待数据的重新加载。 你可以使用LoaderManager.LoaderCallbacks方法 当你知道合何时需要创建新的loader,以及高速应用何时停止使用loader的数据。

LoaderManager.LoaderCallbacks包括以下方法:

  • onCreateLoader() — 根据传入的ID初始化并返回一个新的Loader
    • onLoadFinished() — 当一个之前被创建的loader已经结束加载数据的时候会调用此方法。
    • onLoaderReset() — 当一个之前创建的loader被重置的时候会调用此方法,这样会导致它的数据不可用。

    在下面的章节中会更详细地描述这些方法的细节

    onCreateLoader

    当你试图操作一个loader的时候,(例如通过initLoader()), 会检查被赋予唯一ID的loader是否存在。如果不存在,它会触发LoaderManager.LoaderCallbacksonCreateLoader()方法。 这是你创建新loader的地方。一般来说被创建的都是CursorLoader,但是你可以实现你自己的Loader子类。

    在本例中,onCreateLoader() 回调方法创建了一个CursorLoader。你必须使用它的构造方法建造这个 CursorLoader,构造方法需要向 which ContentProvider执行一次查询的完整信息作为参数。它还需要:尤其地,它还需要:

    • uri — 要获取的内容的URI。
    • projection — 返回的列组成的列表,传入null将会返回所有列,但是效率很低。
    • selection — 一个声明返回哪些行的过滤器,被格式化成类似SQL中WHERE子句的形式(除了没有WHERE自己)。传入null将会返回给定URI的所有行。
    • selectionArgs — 你可能在Selection中包含一些‘?’,他们将会被selectionArgs的值给替换掉,顺序与它们在selection中出现的顺序一致。 这些值被约束为String类型
    • sortOrder — 怎样给这些行排顺序,被格式化为类似SQL中ORDER BY子句的形式(除了没有ORDER自己)。传入null 将会使用默认的排序方式,可能是没有顺序。

    例如:

     //如果不是null,这就是当前的用户提供的过滤器。
    String mCurFilter;
    ...
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        //当新的Loader需要被创建的时候调用此方法。 
        //本例仅有一个Loader,所以不必关心ID的问题。
        //首先,根据我们是否正在过滤,
        //选择base URI来使用。
        Uri baseUri;
        if (mCurFilter != null) {
            baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                      Uri.encode(mCurFilter));
        } else {
            baseUri = Contacts.CONTENT_URI;
        }
    
        //现在创建并返回一个CursorLoader, 
        //它会创建一个用来显示数据的Cursor。
        String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
                + Contacts.HAS_PHONE_NUMBER + "=1) AND ("
                + Contacts.DISPLAY_NAME + " != '' ))";
        return new CursorLoader(getActivity(), baseUri,
                CONTACTS_SUMMARY_PROJECTION, select, null,
                Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
    }

    onLoadFinished

    当上一个被创建的loader已经结束数据加载的时候调用此方法。这个方法被保证会在提供给这个loader的数据被释放之前调用。 这个时候,你应该移除所有旧数据的使用(因为它们马上就会被释放),但是不应该自己去释放它们,因为它们的loader会做这些事。

    一旦知道了应用将不会再使用这些数据,loader就应该立即释放它们。 例如,数据是来自CursorLoader的一个cursor, 你就不应该再调用close()。如果这个cursor正要在 CursorAdapter中被替换,你应该使用swapCursor()方法使得旧的 Cursor不被关闭。例如:

    // This is the Adapter being used to display the list's data.
    SimpleCursorAdapter mAdapter; ... public void onLoadFinished(Loader<Cursor> loader, Cursor data) { //把新cursor换进来  (一旦我们返回了,框架将会管理 //关闭旧cursor的事情) mAdapter.swapCursor(data); }

    onLoaderReset

    当之前创建的loader被重置使得数据不可用的时候,此方法被调用。这个回调方法让你弄清楚数据何时会被释放,进而你可以移除对它的引用 

    下面的实现调用了参数为nullswapCursor()

    // This is the Adapter being used to display the list's data.
    SimpleCursorAdapter mAdapter;
    ...
    
    public void onLoaderReset(Loader<Cursor> loader) {
        //当最后一个Cursor进入到onLoadFinished()时被调用,
        //Cursor将要被关闭, 我们要确保
        //不会再使用到它。
        mAdapter.swapCursor(null);
    }

    Example


    下面的例子完整实现了一个Fragment显示一个包含了从联系人content provider返回的查询数据的ListView的内容的功能。 它使用一个CursorLoader来管理对provider的查询。

    一个应用想要实现操作用户的联系人,如例子中那样,它的manifest一定要包含 READ_CONTACTS权限。

    public static class CursorLoaderListFragment extends ListFragment
            implements OnQueryTextListener, LoaderManager.LoaderCallbacks<Cursor> {
    
        //这就是用来展示列表信息的Adapter。
        SimpleCursorAdapter mAdapter;
    
        //如果不是null,这就是当前的搜索过滤器。
        String mCurFilter;
    
        @Override public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
    
            //如果没有数据,就给控件一些文本去显示。  
            //在真正的应用中,信息来自应用资源。
            setEmptyText("No phone numbers");
    
            //我们在action bar中显示一个菜单项。
            setHasOptionsMenu(true);
    
            //创建一个新的adapter,我们将用它来显示加载的数据。
            mAdapter = new SimpleCursorAdapter(getActivity(),
                    android.R.layout.simple_list_item_2, null,
                    new String[] { Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS },
                    new int[] { android.R.id.text1, android.R.id.text2 }, 0);
            setListAdapter(mAdapter);
    
            //准备loader, 重连到一个已存在的loader,
            //或者启动一个新的loader。
            getLoaderManager().initLoader(0, null, this);
        }
    
        @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
            //放置一个action bar用于搜索。
            MenuItem item = menu.add("Search");
            item.setIcon(android.R.drawable.ic_menu_search);
            item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
            SearchView sv = new SearchView(getActivity());
            sv.setOnQueryTextListener(this);
            item.setActionView(sv);
        }
    
        public boolean onQueryTextChange(String newText) {
            //action bar上的搜索文本改变的时候被调用。 
            //更新搜索过滤器,并且重启loader用当前的过滤器
            //来做新的查询。
            mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
            getLoaderManager().restartLoader(0, null, this);
            return true;
        }
    
        @Override public boolean onQueryTextSubmit(String query) {
            //不必关心这个方法。
            return true;
        }
    
        @Override public void onListItemClick(ListView l, View v, int position, long id) {
            // Insert desired behavior here.
            Log.i("FragmentComplexList", "Item clicked: " + id);
        }
    
        //我们想获取的联系人中的行数据。
        static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
            Contacts._ID,
            Contacts.DISPLAY_NAME,
            Contacts.CONTACT_STATUS,
            Contacts.CONTACT_PRESENCE,
            Contacts.PHOTO_ID,
            Contacts.LOOKUP_KEY,
        };
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            //当需要创建一个新的loader时被调用。 
            //本例中仅有一个loader,所以我们不必关心ID的问题。
            //首先,根据我们当前是否正在过滤,
            //选择base URI来使用。
            Uri baseUri;
            if (mCurFilter != null) {
                baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                        Uri.encode(mCurFilter));
            } else {
                baseUri = Contacts.CONTENT_URI;
            }
    
            //现在创建并返回一个CursorLoader,
            //它将会为被显示的数据创建一个Cursor。
            String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
                    + Contacts.HAS_PHONE_NUMBER + "=1) AND ("
                    + Contacts.DISPLAY_NAME + " != '' ))";
            return new CursorLoader(getActivity(), baseUri,
                    CONTACTS_SUMMARY_PROJECTION, select, null,
                    Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
        }
    
        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            //把新的cursor换进来。  (框架将会在我们返回的时候
            //管理旧cursor的关闭事宜。)
            mAdapter.swapCursor(data);
        }
    
        public void onLoaderReset(Loader<Cursor> loader) {
            //当最后一个Cursor进入onLoadFinished()的时候被调用。
            //cursor将要被关闭, 我们应该确保
            //不再使用它。
            mAdapter.swapCursor(null);
        }
    }

    更多实例

    ApiDemos中有一些不同的例子,它们说明了怎样使用loader:

    • LoaderCursor — 上面展示的片段的汇总的完整版本。
    • LoaderThrottle — 一个例子,用来展示当数据改变时,怎样使用throtting来减少对content provider做查询的次数

    下载以及安装SDK实例的信息,请参看 Getting the Samples