加载器

Android P(API 级别 28)中已弃用加载器。在处理 Activity 和 Fragment 生命周期时,推荐使用 ViewModelsLiveData 的组合来处理加载数据。当加载器等配置发生变更后,ViewModel 仍会存在,但其样板文件会有所减少。LiveData 提供拥有生命周期感知能力的数据加载方法,您可以在多个 ViewModel 中重复使用该方法。您还可使用 MediatorLiveData 结合 LiveData,并且可使用任何可观察查询(例如来自 Room 数据库的查询)来观察数据变更。ViewModel 和 LiveData 也适用于您无权访问 LoaderManager 的情况,例如,在 Service 中。若将二者结合使用,您便可轻松访问应用所需的数据,而无需处理界面生命周期。如要了解关于 LiveData 和 ViewModel 的更多信息,请分别参阅 LiveData 指南ViewModel 指南

借助 Loader API,您可以从内容提供程序或其他数据源中加载数据,以便在 FragmentActivityFragment 中显示。如果您不理解为何需要 Loader API 来执行这个看似无关紧要的操作,请首先考虑没有加载器时可能会遇到的一些问题:

  • 如果直接在 Activity 或片段中获取数据,由于通过界面线程执行查询的速度可能较慢,响应能力的不足将影响您的用户。
  • 如果从另一个线程获取数据(方法可能是使用 AsyncTask),则您需负责通过各种 Activity或片段生命周期事件(例如 onDestroy() 和配置变更)来管理线程和界面线程。

加载器不仅能解决这些问题,同时还具备其他优势。例如:

  • 加载器在单独的线程上运行,以免界面出现卡顿或无响应问题。
  • 加载器可在事件发生时提供回调方法,从而简化线程管理。
  • 加载器会保留和缓存配置变更后的结果,以免出现重复查询问题。
  • 加载器可实现观察器,从而监控基础数据源的变化。例如,CursorLoader 会自动注册 ContentObserver,以在数据变更时触发重新加载。

Loader API 摘要

在应用中使用加载器时,可能会涉及到多个类和接口。下表对其进行了总结:

类/接口 描述
LoaderManager 一种与 FragmentActivityFragment 相关联的抽象类,用于管理一个或多个 Loader 实例。每个 Activity 或片段只有一个 LoaderManager,但 LoaderManager 可管理多个加载器。

如要获取 LoaderManager,请从 Activity 或片段调用 getSupportLoaderManager()

如要从加载器开始加载数据,请调用 initLoader()restartLoader()。系统会自动确定是否已存在拥有相同整型 ID 的加载器,并将创建新加载器或重复使用现有的加载器。

LoaderManager.LoaderCallbacks 此接口包含加载器事件发生时所调用的回调方法。接口定义三种回调方法: 此接口一般由您的 Activity 或片段实现,并在您调用 initLoader()restartLoader() 时进行注册。
Loader Loader 类执行数据的加载。此类属于抽象类,并且是所有加载器的基类。您可以直接创建 Loader 的子类,或使用以下某个内置子类来简化实现:

下文将为您展示如何在应用中使用这些类和接口。

在应用中使用加载器

此部分描述如何在 Android 应用中使用加载器。使用加载器的应用通常包含以下内容:

启动加载器

LoaderManager 可在 FragmentActivityFragment 内管理一个或多个 Loader 实例。每个 Activity 或片段中只有一个 LoaderManager

通常,您需在 Activity 的 onCreate() 方法或片段的 onActivityCreated() 方法内初始化 Loader。您可以执行如下操作:

Kotlin

supportLoaderManager.initLoader(0, null, this)

Java

// Prepare the loader.  Either re-connect with an existing one,
// or start a new one.
getSupportLoaderManager().initLoader(0, null, this);

initLoader() 方法采用以下参数:

  • 用于标识加载器的唯一 ID。在此示例中,ID 为 0。
  • 在构建时提供给加载器的可选参数(在此示例中为 null)。
  • LoaderManager.LoaderCallbacks 实现,LoaderManager 将调用此实现来报告加载器事件。在此示例中,本地类实现 LoaderManager.LoaderCallbacks 接口,因此其传递对自身的引用 this

initLoader() 调用确保加载器已初始化且处于活动状态。这可能会出现两种结果:

无论是哪种情况,给定的 LoaderManager.LoaderCallbacks 实现均与加载器相关联,且将在加载器状态变化时进行调用。在进行这次调用时,如果调用方处于启动状态,且请求的加载器已存在并生成了数据,则系统会(在 initLoader() 期间)立即调用 onLoadFinished(),因此您必须为此做好准备。有关此回调的详细介绍,请参阅 onLoadFinished

请注意,initLoader() 方法会返回已创建的 Loader,但您无需捕获其引用。LoaderManager 会自动管理加载器的生命周期。LoaderManager 会根据需要启动和停止加载,并维护加载器的状态及其相关内容。这意味着您很少直接与加载器进行交互(有关使用加载器方法调整加载器行为的示例,请参阅 LoaderThrottle 示例)。当特殊事件发生时,您通常会使用 LoaderManager.LoaderCallbacks 方法干预加载进程。有关此主题的详细介绍,请参阅使用 LoaderManager 回调

重启加载器

当您使用 initLoader() 时(如上所述),它会使用包含指定 ID(如有)的现有加载器。如果没有,则它会创建 ID。但有时,您想要舍弃这些旧数据并重新开始。

如要舍弃旧数据,请使用 restartLoader()。例如,当用户的查询发生变更时,此 SearchView.OnQueryTextListener 实现将重启加载器。经重启后,加载器便可使用改进的搜索过滤器执行新查询:

Kotlin

fun onQueryTextChanged(newText: String?): Boolean {
    // Called when the action bar search text has changed.  Update
    // the search filter, and restart the loader to do a new query
    // with this filter.
    curFilter = if (newText?.isNotEmpty() == true) newText else null
    supportLoaderManager.restartLoader(0, null, this)
    return true
}

Java

public boolean onQueryTextChanged(String newText) {
    // Called when the action bar search text has changed.  Update
    // the search filter, and restart the loader to do a new query
    // with this filter.
    curFilter = !TextUtils.isEmpty(newText) ? newText : null;
    getSupportLoaderManager().restartLoader(0, null, this);
    return true;
}

使用 LoaderManager 回调

LoaderManager.LoaderCallbacks 是一个回调接口,可让客户端与 LoaderManager 进行交互。

加载器(特别是 CursorLoader)在停止运行后,仍需保留其数据。如此一来,应用即可保留 Activity 或片段的 onStop()onStart() 方法中的数据,以便用户在返回应用时无需等待其重新加载这些数据。您可以使用 LoaderManager.LoaderCallbacks 方法了解何时创建新加载器,并告知应用何时停止使用加载器的数据。

LoaderManager.LoaderCallbacks 包含以下方法:

  • onLoaderReset():当先前创建的加载器重置且因此其数据不可用时调用。

下文将更详细地描述这些方法。

onCreateLoader

当您尝试(例如,通过 initLoader())访问加载器时,该方法将检查是否已存在由该 ID 指定的加载器。如果没有,它将触发 LoaderManager.LoaderCallbacks 方法 onCreateLoader()。在此方法中,您可以创建新加载器。新加载器通常是 CursorLoader,但您也可实现自己的 Loader 子类。

在此示例中,onCreateLoader() 回调方法创建了 CursorLoader。您必须使用其构造函数方法来构建 CursorLoader,而该方法需要对 ContentProvider 执行查询时所需的一系列完整信息。具体而言,该方法需要:

  • uri:用于检索内容的 URI
  • projection:要返回的列的列表。传递 null 时,将返回所有列,这样会导致效率低下。
  • selection:一种用于声明要返回哪些行的过滤器,采用 SQL WHERE 子句格式(WHERE 自身除外)。传递 null 时,将为指定的 URI 返回所有行。
  • selectionArgs:您可以在 selection 中包含 ?,它将按照在 selection 中显示的顺序替换为 selectionArgs 中的值。该值将会绑定为字串符。
  • sortOrder:行的排序依据,采用 SQLORDER BY 子句格式(ORDER BY 自身除外)。传递 null 时,将使用默认排序顺序(可能并未排序)

例如:

Kotlin

// If non-null, this is the current filter the user has provided.
private var curFilter: String? = null
...
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
    // This is called when a new Loader needs to be created.  This
    // sample only has one Loader, so we don't care about the ID.
    // First, pick the base URI to use depending on whether we are
    // currently filtering.
    val baseUri: Uri = if (curFilter != null) {
        Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, Uri.encode(curFilter))
    } else {
        ContactsContract.Contacts.CONTENT_URI
    }

    // Now create and return a CursorLoader that will take care of
    // creating a Cursor for the data being displayed.
    val select: String = "((${Contacts.DISPLAY_NAME} NOTNULL) AND (" +
            "${Contacts.HAS_PHONE_NUMBER}=1) AND (" +
            "${Contacts.DISPLAY_NAME} != ''))"
    return (activity as? Context)?.let { context ->
        CursorLoader(
                context,
                baseUri,
                CONTACTS_SUMMARY_PROJECTION,
                select,
                null,
                "${Contacts.DISPLAY_NAME} COLLATE LOCALIZED ASC"
        )
    } ?: throw Exception("Activity cannot be null")
}

Java

// If non-null, this is the current filter the user has provided.
String curFilter;
...
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    // This is called when a new Loader needs to be created.  This
    // sample only has one Loader, so we don't care about the ID.
    // First, pick the base URI to use depending on whether we are
    // currently filtering.
    Uri baseUri;
    if (curFilter != null) {
        baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                  Uri.encode(curFilter));
    } else {
        baseUri = Contacts.CONTENT_URI;
    }

    // Now create and return a CursorLoader that will take care of
    // creating a Cursor for the data being displayed.
    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

当先前创建的加载器完成加载时,将调用此方法。只有在释放为此加载器提供的最后一个数据后,您才可调用此方法。此时,您应移除所有使用的旧数据(因为它们很快会被释放),但请勿自行释放这些数据,因为这些数据归其加载器所有,该加载器会对其进行处理。

当加载器发现应用不再使用这些数据时,即会将其释放。例如,如果数据是来自 CursorLoader 的游标,则您不应手动对其调用 close()。如果游标放置在 CursorAdapter 中,则您应使用 swapCursor() 方法,这样旧 Cursor 便不会关闭。例如:

Kotlin

private lateinit var adapter: SimpleCursorAdapter
...

override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
    // Swap the new cursor in.  (The framework will take care of closing the
    // old cursor once we return.)
    adapter.swapCursor(data)
}

Java

// This is the Adapter being used to display the list's data.
SimpleCursorAdapter adapter;
...

public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    // Swap the new cursor in.  (The framework will take care of closing the
    // old cursor once we return.)
    adapter.swapCursor(data);
}

onLoaderReset

当重置先前创建的加载器并因此导致其数据不可用时,将调用此方法。借助此回调,您可以了解何时将释放数据,以便及时移除其引用。  

此实现会调用值为 nullswapCursor()

Kotlin

private lateinit var adapter: SimpleCursorAdapter
...

override fun onLoaderReset(loader: Loader<Cursor>) {
    // This is called when the last Cursor provided to onLoadFinished()
    // above is about to be closed.  We need to make sure we are no
    // longer using it.
    adapter.swapCursor(null)
}

Java

// This is the Adapter being used to display the list's data.
SimpleCursorAdapter adapter;
...

public void onLoaderReset(Loader<Cursor> loader) {
    // This is called when the last Cursor provided to onLoadFinished()
    // above is about to be closed.  We need to make sure we are no
    // longer using it.
    adapter.swapCursor(null);
}

示例

以下是一个 Fragment 的完整实现示例,该示例展示了一个 ListView,其中包含针对联系人内容提供程序的查询结果。该示例使用 CursorLoader 来管理提供程序的查询。

应用如需访问用户联系人(如此示例中所示),则其清单文件必须包括 READ_CONTACTS 权限。

Kotlin

private val CONTACTS_SUMMARY_PROJECTION: Array<String> = arrayOf(
        Contacts._ID,
        Contacts.DISPLAY_NAME,
        Contacts.CONTACT_STATUS,
        Contacts.CONTACT_PRESENCE,
        Contacts.PHOTO_ID,
        Contacts.LOOKUP_KEY
)


class CursorLoaderListFragment :
        ListFragment(),
        SearchView.OnQueryTextListener,
        LoaderManager.LoaderCallbacks<Cursor> {

    // This is the Adapter being used to display the list's data.
    private lateinit var mAdapter: SimpleCursorAdapter

    // If non-null, this is the current filter the user has provided.
    private var curFilter: String? = null

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        // Give some text to display if there is no data.  In a real
        // application this would come from a resource.
        setEmptyText("No phone numbers")

        // We have a menu item to show in action bar.
        setHasOptionsMenu(true)

        // Create an empty adapter we will use to display the loaded data.
        mAdapter = SimpleCursorAdapter(activity,
                android.R.layout.simple_list_item_2,
                null,
                arrayOf(Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS),
                intArrayOf(android.R.id.text1, android.R.id.text2),
                0
        )
        listAdapter = mAdapter

        // Prepare the loader.  Either re-connect with an existing one,
        // or start a new one.
        loaderManager.initLoader(0, null, this)
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        // Place an action bar item for searching.
        menu.add("Search").apply {
            setIcon(android.R.drawable.ic_menu_search)
            setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
            actionView = SearchView(activity).apply {
                setOnQueryTextListener(this@CursorLoaderListFragment)
            }
        }
    }

    override fun onQueryTextChange(newText: String?): Boolean {
        // Called when the action bar search text has changed.  Update
        // the search filter, and restart the loader to do a new query
        // with this filter.
        curFilter = if (newText?.isNotEmpty() == true) newText else null
        loaderManager.restartLoader(0, null, this)
        return true
    }

    override fun onQueryTextSubmit(query: String): Boolean {
        // Don't care about this.
        return true
    }

    override fun onListItemClick(l: ListView, v: View, position: Int, id: Long) {
        // Insert desired behavior here.
        Log.i("FragmentComplexList", "Item clicked: $id")
    }

    override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
        // This is called when a new Loader needs to be created.  This
        // sample only has one Loader, so we don't care about the ID.
        // First, pick the base URI to use depending on whether we are
        // currently filtering.
        val baseUri: Uri = if (curFilter != null) {
            Uri.withAppendedPath(Contacts.CONTENT_URI, Uri.encode(curFilter))
        } else {
            Contacts.CONTENT_URI
        }

        // Now create and return a CursorLoader that will take care of
        // creating a Cursor for the data being displayed.
        val select: String = "((${Contacts.DISPLAY_NAME} NOTNULL) AND (" +
                "${Contacts.HAS_PHONE_NUMBER}=1) AND (" +
                "${Contacts.DISPLAY_NAME} != ''))"
        return (activity as? Context)?.let { context ->
            CursorLoader(
                    context,
                    baseUri,
                    CONTACTS_SUMMARY_PROJECTION,
                    select,
                    null,
                    "${Contacts.DISPLAY_NAME} COLLATE LOCALIZED ASC"
            )
        } ?: throw Exception("Activity cannot be null")
    }

    override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor) {
        // Swap the new cursor in.  (The framework will take care of closing the
        // old cursor once we return.)
        mAdapter.swapCursor(data)
    }

    override fun onLoaderReset(loader: Loader<Cursor>) {
        // This is called when the last Cursor provided to onLoadFinished()
        // above is about to be closed.  We need to make sure we are no
        // longer using it.
        mAdapter.swapCursor(null)
    }
}

Java

public static class CursorLoaderListFragment extends ListFragment
        implements OnQueryTextListener, LoaderManager.LoaderCallbacks<Cursor> {

    // This is the Adapter being used to display the list's data.
    SimpleCursorAdapter mAdapter;

    // If non-null, this is the current filter the user has provided.
    String curFilter;

    @Override public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        // Give some text to display if there is no data.  In a real
        // application this would come from a resource.
        setEmptyText("No phone numbers");

        // We have a menu item to show in action bar.
        setHasOptionsMenu(true);

        // Create an empty adapter we will use to display the loaded data.
        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);

        // Prepare the loader.  Either re-connect with an existing one,
        // or start a new one.
        getLoaderManager().initLoader(0, null, this);
    }

    @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        // Place an action bar item for searching.
        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) {
        // Called when the action bar search text has changed.  Update
        // the search filter, and restart the loader to do a new query
        // with this filter.
        curFilter = !TextUtils.isEmpty(newText) ? newText : null;
        getLoaderManager().restartLoader(0, null, this);
        return true;
    }

    @Override public boolean onQueryTextSubmit(String query) {
        // Don't care about this.
        return true;
    }

    @Override public void onListItemClick(ListView l, View v, int position, long id) {
        // Insert desired behavior here.
        Log.i("FragmentComplexList", "Item clicked: " + id);
    }

    // These are the Contacts rows that we will retrieve.
    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) {
        // This is called when a new Loader needs to be created.  This
        // sample only has one Loader, so we don't care about the ID.
        // First, pick the base URI to use depending on whether we are
        // currently filtering.
        Uri baseUri;
        if (curFilter != null) {
            baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                    Uri.encode(curFilter));
        } else {
            baseUri = Contacts.CONTENT_URI;
        }

        // Now create and return a CursorLoader that will take care of
        // creating a Cursor for the data being displayed.
        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) {
        // Swap the new cursor in.  (The framework will take care of closing the
        // old cursor once we return.)
        mAdapter.swapCursor(data);
    }

    public void onLoaderReset(Loader<Cursor> loader) {
        // This is called when the last Cursor provided to onLoadFinished()
        // above is about to be closed.  We need to make sure we are no
        // longer using it.
        mAdapter.swapCursor(null);
    }
}

更多示例

以下示例说明如何使用加载器: