从 Android 9(API 级别 28)开始废弃了加载器。如需在处理 Activity
和 Fragment
生命周期时加载数据,建议将 ViewModel
对象和 LiveData
组合使用。视图模型在发生配置更改后仍然存在(例如加载器),但样板代码更少。LiveData
提供了一种生命周期感知型数据加载方法,您可以在多个视图模型中重复使用该方法。您还可以使用 MediatorLiveData
组合 LiveData
。任何可观察的查询(例如来自 Room 数据库的查询)都可用于观察数据更改。
在您无权访问 LoaderManager
的情况下(例如在 Service
中),也可以使用 ViewModel
和 LiveData
。结合使用这两者,可以轻松访问应用所需的数据,而无需处理界面生命周期。如需详细了解 LiveData
,请参阅 LiveData
概览。如需详细了解 ViewModel
,请参阅 ViewModel
概览。
借助 Loader API,您可以从 content provider 或其他数据源加载数据,以便在 FragmentActivity
或 Fragment
中显示。
如果没有加载器,您可能会遇到的一些问题包括:
- 如果您直接在 Activity 或 Fragment 中提取数据,由于通过界面线程执行查询的速度可能会很慢,因此用户会遇到响应速度缓慢的问题。
- 如果您从其他线程(可能是使用
AsyncTask
)提取数据,则需要通过各种 activity 或 fragment 生命周期事件(例如onDestroy()
和配置更改)来管理该线程和界面线程。
加载器可以解决这些问题,并且具有其他优势:
- 加载器在单独的线程上运行,以防止界面运行缓慢或无响应。
- 加载器通过在事件发生时提供回调方法来简化线程管理。
- 在配置更改后,加载器会保留并缓存结果,以防查询重复。
- 加载器可以实现一个观察器来监控底层数据源的变化。例如,
CursorLoader
会自动注册ContentObserver
,以在数据发生更改时触发重新加载。
Loader API 摘要
在应用中使用加载器时,可能会涉及多个类和接口。下表对其进行了总结:
类/接口 | 说明 |
---|---|
LoaderManager |
与 FragmentActivity 或 Fragment 关联的抽象类,用于管理一个或多个 Loader 实例。每个 activity 或 fragment 只有一个 LoaderManager ,但一个 LoaderManager 可以管理多个加载器。
如需获取 如需开始从加载器加载数据,请调用 |
LoaderManager.LoaderCallbacks |
此接口包含在发生加载器事件时调用的回调方法。该接口定义了三种回调方法:
initLoader() 或 restartLoader() 时注册。
|
Loader |
加载器用于加载数据。此类是抽象类,用作所有加载器的基类。您可以直接创建 Loader 的子类或使用以下某个内置子类来简化实现:
|
以下各部分将介绍如何在应用中使用这些类和接口。
在应用中使用加载器
本部分介绍了如何在 Android 应用中使用加载器。使用加载器的应用通常包含以下内容:
FragmentActivity
或Fragment
。LoaderManager
的实例。CursorLoader
,用于加载由ContentProvider
支持的数据。或者,您也可以实现自己的Loader
或AsyncTaskLoader
子类,从其他来源加载数据。LoaderManager.LoaderCallbacks
的实现。 您可以在此处创建新加载器并管理对现有加载器的引用。- 一种显示加载器数据的方法,如
SimpleCursorAdapter
。 - 使用
CursorLoader
时的数据源,例如ContentProvider
。
启动加载器
LoaderManager
在 FragmentActivity
或 Fragment
中管理一个或多个 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
包含以下方法:
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
此方法将在先前创建的加载器重置且其数据因此不可用时调用。通过此回调,您可以了解何时将释放数据,以便移除对它的引用。
此实现会调用值为 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
的完整实现,该实现显示一个 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); } }
更多示例
以下示例说明如何使用加载器:
- LoaderCursor:上述代码段的完整版本。
- 检索联系人列表:使用
CursorLoader
从联系人提供程序中检索数据的演示。 - LoaderThrottle:此示例说明了如何在数据发生更改时,使用限制来减少 content provider 执行的查询次数。