加载器

从 Android 9(API 级别 28)开始,加载器已废弃。对于 在处理 ActivityFragment 生命周期的同时处理加载数据,使用的是 ViewModel 对象组合 和 LiveData。 视图模型在加载器等配置更改后仍然存在, 减少样板代码LiveData 提供了一种具有生命周期感知能力的数据加载方法,您可在 多个视图模型。您还可以使用以下代码组合 LiveDataMediatorLiveData。 任何可观察查询,例如来自 Room 数据库,可用于观察更改 数据。

在您无权访问的情况下,也可以使用 ViewModelLiveData 传递到 LoaderManager 中,例如在 Service。使用 tandem 提供了一种无需处理界面即可访问应用所需数据的简单方法 生命周期如需详细了解 LiveData,请参阅 LiveData 概览。要详细了解 ViewModel,请参阅 ViewModel 概览

利用 Loader API,您可以从 content provider 或其他数据源,以便在 FragmentActivity 中显示 或 Fragment

如果没有加载器,您可能会遇到的一些问题包括:

  • 如果您直接在 activity 或 fragment 中提取数据,您的用户 因为运行速度可能较慢, 从界面线程执行查询
  • 如果您从其他线程获取数据(可能使用 AsyncTask), 那么你会负责管理 以及通过各种 activity 或 fragment 生命周期事件来管理界面线程,例如 onDestroy() 和配置更改。

加载器可以解决这些问题,还可以带来其他好处:

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

Loader API 摘要

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

类/接口 说明
LoaderManager FragmentActivityFragment,用于管理一个或多个 Loader 个实例。只有一个 每个 activity 或 fragment LoaderManager,但 LoaderManager 可以管理多个加载器。

如需获取 LoaderManager,请调用 getSupportLoaderManager() 从 activity 或 fragment 中获取。

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

LoaderManager.LoaderCallbacks 该接口包含回调方法, 加载程序事件发生时抛出的异常。该接口定义了三种回调方法: <ph type="x-smartling-placeholder">。 您的 activity 或 fragment 通常会实现此接口,并且 您在调用 initLoader()restartLoader()
Loader 加载器用于加载数据。这个类是抽象类, 作为所有加载器的基类。您可以直接创建子类 Loader 或使用以下任一内置功能 子类以简化实现: <ph type="x-smartling-placeholder">

以下部分将介绍如何使用 类和接口

在应用中使用加载器

本部分介绍了如何在 Android 应用中使用加载器。一个 使用加载器的应用通常包含以下内容:

启动加载器

LoaderManager 管理FragmentActivityLoader Fragment。每个 activity 或 fragment 只有一个 LoaderManager

您通常 在 activity 的 onCreate() 方法中或 fragment 的Loader onCreate() 方法。您 按如下方式执行此操作:

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 与加载器相关联,会在加载时调用 加载器状态更改。如果在此调用时,调用方处于 且请求的加载器已存在,且已生成 数据,系统会调用 onLoadFinished()initLoader()期间立即发生。您必须对此做好准备。有关此回调的详细介绍,请参阅有关 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:如果您在所选内容中包含 ?s,则它们 会按照它们的出现顺序替换为 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: 针对联系人 content provider 的查询结果。它使用 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);
    }
}

更多示例

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