加载器

从 Android 9(API 级别 28)开始,加载器已弃用。处理加载数据时处理 ActivityFragment 生命周期的推荐选项是结合使用 ViewModel 对象和 LiveData。视图模型在配置更改(如加载器)中仍然存在,但代码更简洁。LiveData 提供了一种生命周期感知的方式来加载数据,您可以在多个视图模型中重复使用它。您还可以使用 MediatorLiveData 组合 LiveData。任何可观察的查询(例如来自 Room 数据库 的查询)都可以用于观察数据的更改。

ViewModelLiveData 在您无法访问 LoaderManager 的情况下(例如在 Service 中)也可用。将两者结合使用提供了一种简单的方法来访问应用程序所需的数据,而无需处理 UI 生命周期。要了解有关 LiveData 的更多信息,请参阅 LiveData 概述。要了解有关 ViewModel 的更多信息,请参阅 ViewModel 概述

Loader API 允许您加载来自 内容提供程序 或其他数据源的数据,以在 FragmentActivityFragment 中显示。

如果没有加载器,您可能会遇到以下一些问题

  • 如果您在活动或片段中直接获取数据,则用户会由于在 UI 线程中执行潜在的缓慢查询而导致响应能力下降。
  • 如果您从另一个线程(也许使用 AsyncTask)获取数据,那么您需要通过各种活动或片段生命周期事件(例如 onDestroy() 和配置更改)来管理该线程和 UI 线程。

加载器解决了这些问题并包含其他好处

  • 加载器在单独的线程上运行,以防止 UI 缓慢或无响应。
  • 加载器通过在事件发生时提供回调方法来简化线程管理。
  • 加载器在配置更改期间保留和缓存结果,以防止重复查询。
  • 加载器可以实现一个观察器来监视底层数据源的变化。例如,CursorLoader 会自动注册一个 ContentObserver,以便在数据更改时触发重新加载。

Loader API 摘要

在应用程序中使用加载器时,可能涉及多个类和接口。它们在以下表格中进行了总结

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

要获取 LoaderManager,请从活动或片段中调用 getSupportLoaderManager()

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

LoaderManager.LoaderCallbacks 此接口包含在加载器事件发生时调用的回调方法。该接口定义了三个回调方法您的活动或片段通常实现此接口,并且在您调用 initLoader()restartLoader() 时注册。
Loader 加载器执行数据加载。此类是抽象的,用作所有加载器的基类。您可以直接子类化 Loader 或使用以下内置子类之一来简化实现

以下部分将向您展示如何在应用程序中使用这些类和接口。

在应用程序中使用加载器

本节介绍如何在 Android 应用程序中使用加载器。使用加载器的应用程序通常包括以下内容

启动加载器

LoaderManagerFragmentActivityFragment 中管理一个或多个 Loader 实例。每个活动或片段只有一个 LoaderManager

您通常在活动的 onCreate() 方法或片段的 onCreate() 方法中初始化 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() 调用确保加载器已初始化并处于活动状态。它有两个可能的结果

  • 如果由 ID 指定的加载器已存在,则重用最后创建的加载器。
  • 如果由 ID 指定的加载器*不存在*,则 initLoader() 会触发 LoaderManager.LoaderCallbacks 方法 onCreateLoader()。在这里,您实现代码以实例化并返回一个新的加载器。有关更多讨论,请参阅有关 onCreateLoader 的部分。

在这两种情况下,给定的 LoaderManager.LoaderCallbacks 实现都与加载器关联,并在加载器状态更改时调用。如果在调用此方法时,调用方处于其已启动状态,并且请求的加载器已存在并已生成其数据,则系统会立即在 initLoader() 期间调用 onLoadFinished()。您必须准备好发生这种情况。有关此回调的更多讨论,请参阅有关 onLoadFinished 的部分。

initLoader() 方法返回创建的 Loader,但您不需要捕获对它的引用。LoaderManager 自动管理加载器的生命周期。LoaderManager 会在必要时启动和停止加载,并维护加载器及其关联内容的状态。

正如这所暗示的那样,您很少直接与加载器交互。您最常使用 LoaderManager.LoaderCallbacks 方法在发生特定事件时干预加载过程。有关此主题的更多讨论,请参阅 使用 LoaderManager 回调 部分。

重新启动加载器

当您使用 initLoader()(如上一节所示)时,如果存在具有指定 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 或 Fragment 的onStop()onStart() 方法之间保持其数据,以便当用户返回应用程序时,他们不必等待数据重新加载。

您可以使用LoaderManager.LoaderCallbacks 方法来了解何时创建新的加载器,以及何时通知应用程序停止使用加载器的数据。

LoaderManager.LoaderCallbacks 包含以下方法

  • onLoaderReset():当先前创建的加载器正在重置时调用,从而使其数据不可用。

这些方法将在以下部分中详细介绍。

onCreateLoader

当您尝试访问加载器(例如通过initLoader())时,它会检查指定 ID 的加载器是否存在。如果不存在,则会触发LoaderManager.LoaderCallbacks 方法onCreateLoader()。在这里,您可以创建一个新的加载器。通常,这是一个CursorLoader,但您也可以实现自己的Loader 子类。

在以下示例中,onCreateLoader() 回调方法使用其构造方法创建一个CursorLoader,该方法需要执行对ContentProvider 的查询所需的信息集。具体来说,它需要以下内容:

  • uri:要检索内容的 URI。
  • projection:要返回的列列表。传递null 将返回所有列,这效率低下。
  • selection:一个过滤器,声明要返回哪些行,格式为 SQL WHERE 子句(不包括 WHERE 本身)。传递null 将返回给定 URI 的所有行。
  • selectionArgs:如果您在 selection 中包含 ?,则它们将按其在 selection 中出现的顺序由 selectionArgs 中的值替换。这些值绑定为字符串。
  • sortOrder:如何对行进行排序,格式为 SQL ORDER 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

当先前创建的加载器正在重置时,将调用此方法,从而使其数据不可用。此回调允许您了解数据何时即将被释放,以便您可以删除对它的引用。

此实现使用null 的值调用swapCursor()

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,该 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 onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, 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
    }

    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 onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

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

    @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, 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);
    }

    @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);
    }
}

更多示例

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