从 Android 9(API 级别 28)开始,加载器已废弃。对于
在处理 Activity
和 Fragment
生命周期的同时处理加载数据,使用的是
ViewModel
对象组合
和 LiveData
。
视图模型在加载器等配置更改后仍然存在,
减少样板代码LiveData
提供了一种具有生命周期感知能力的数据加载方法,您可在
多个视图模型。您还可以使用以下代码组合 LiveData
:
MediatorLiveData
。
任何可观察查询,例如来自
Room 数据库,可用于观察更改
数据。
在您无权访问的情况下,也可以使用 ViewModel
和 LiveData
传递到 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 |
与 FragmentActivity 或
Fragment ,用于管理一个或多个
Loader 个实例。只有一个
每个 activity 或 fragment LoaderManager ,但
LoaderManager 可以管理多个加载器。
如需获取 如需开始从加载器加载数据,请调用
|
LoaderManager.LoaderCallbacks |
该接口包含回调方法,
加载程序事件发生时抛出的异常。该接口定义了三种回调方法:
<ph type="x-smartling-placeholder">
initLoader() 或
restartLoader() 。
|
Loader |
加载器用于加载数据。这个类是抽象类,
作为所有加载器的基类。您可以直接创建子类
Loader 或使用以下任一内置功能
子类以简化实现:
<ph type="x-smartling-placeholder">
|
以下部分将介绍如何使用 类和接口
在应用中使用加载器
本部分介绍了如何在 Android 应用中使用加载器。一个 使用加载器的应用通常包含以下内容:
FragmentActivity
或Fragment
。LoaderManager
的实例。CursorLoader
,用于加载由ContentProvider
支持的数据。或者,您也可以实现自己的子类 /Loader
或AsyncTaskLoader
至 从其他来源加载数据。LoaderManager.LoaderCallbacks
的实现。 您可以在此处创建新加载器并管理对现有加载器的引用, 加载器。- 一种显示加载器数据的方法,例如
SimpleCursorAdapter
。 - 使用
ContentProvider
等数据源时CursorLoader
。
启动加载器
LoaderManager
管理FragmentActivity
或Loader
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
包含以下内容
方法:
onCreateLoader()
: 针对指定 ID 实例化并返回新的Loader
。
-
onLoadFinished()
: 在先前创建的加载器完成加载时调用。
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); } }
更多示例
以下示例说明如何使用加载器:
- <ph type="x-smartling-placeholder"></ph> LoaderCursor:上述代码段的完整版本。
- 检索联系人列表:
使用
CursorLoader
检索 来自联系人提供程序的数据。 - <ph type="x-smartling-placeholder"></ph> LoaderThrottle:通过限制功能来减少 content provider 在其数据发生更改时执行的查询。