加载器

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

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

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

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

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

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

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

加载器 API 摘要

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

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

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

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

LoaderManager.LoaderCallbacks 此接口包含在加载器事件发生时调用的回调方法。该接口定义了三个回调方法
  • onCreateLoader(int, Bundle):当系统需要创建一个新的加载器时调用。在您的代码中,创建一个Loader对象并将其返回给系统。
  • onLoadFinished(Loader<D>, D):当加载器完成数据加载时调用。您通常在代码中将数据显示给用户。
  • onLoaderReset(Loader<D>):当先前创建的加载器被重置时调用,当您调用destroyLoader(int)时或当活动或片段被销毁时(使其数据不可用)。在您的代码中,删除对加载器数据的任何引用。
您的活动或片段通常实现此接口,并在您调用initLoader()restartLoader()时注册。
Loader 加载器执行数据加载。此类是抽象的,并且用作所有加载器的基类。您可以直接子类化Loader或使用以下内置子类之一来简化实现

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

在应用程序中使用加载器

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

启动加载器

LoaderManagerFragmentActivityFragment 中管理一个或多个 Loader 实例。每个 Activity 或 Fragment 只有一个 LoaderManager

通常在 Activity 的 onCreate() 方法或 Fragment 的 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

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

此实现使用 swapCursor() 并传入 null 值。

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

更多示例

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