載入器已於 Android 9 (API 級別 28) 淘汰。如要在處理 Activity
和 Fragment
生命週期的同時載入資料,建議您使用 ViewModel
物件和 LiveData
的組合。載入模型等設定變更後,檢視畫面模型仍會保留,但樣板程式碼較少。LiveData
提供一種生命週期感知方式載入資料,可讓您在多個檢視模型中重複使用。您也可以使用 MediatorLiveData
合併 LiveData
。任何可觀測的查詢 (例如 Room 資料庫中的查詢) 都可用來觀察資料的變更。
ViewModel
和 LiveData
也適用於您無法存取 LoaderManager
(例如在 Service
中) 的情況。同時使用這兩種模式可讓您輕鬆存取應用程式所需的資料,而無需處理 UI 生命週期。如要進一步瞭解 LiveData
,請參閱「LiveData
總覽」。如要進一步瞭解 ViewModel
,請參閱「ViewModel
總覽」。
Loader API 可讓您從內容供應器或其他資料來源載入資料,以便在 FragmentActivity
或 Fragment
中顯示。
如果沒有載入器,可能會遇到下列問題:
- 如果您直接在活動或片段中擷取資料,使用者因為從 UI 執行緒執行查詢的速度可能過慢,導致回應速度不夠快。
- 如果您從其他執行緒擷取資料 (可能使用
AsyncTask
),則您必須負責透過各種活動或片段生命週期事件 (例如onDestroy()
和設定變更) 管理該執行緒和 UI 執行緒。
載入器可以解決這些問題,並提供其他好處:
- 載入器會在個別執行緒上執行,避免 UI 速度緩慢或無回應。
- 載入器會在事件發生時提供回呼方法,藉此簡化執行緒管理。
- 載入器會在設定變更時保留並快取結果,以免查詢重複。
- 載入器可以實作觀察器,監控基礎資料來源的變化。舉例來說,
CursorLoader
會自動註冊ContentObserver
,在資料變更時觸發重新載入。
Loader API 摘要
在應用程式中使用載入器時,可能會涉及多個類別和介面。下表摘要說明:
類別/介面 | 說明 |
---|---|
LoaderManager |
與 FragmentActivity 或 Fragment 相關聯的抽象類別,用於管理一或多個 Loader 執行個體。每個活動或片段只有一個 LoaderManager ,但 LoaderManager 可以管理多個載入器。如要取得 如要開始從載入器載入資料,請呼叫 |
LoaderManager.LoaderCallbacks |
此介麵包含載入器事件時呼叫的回呼方法。介面會定義三種回呼方法:
initLoader() 或 restartLoader() 時註冊。 |
Loader |
載入器會執行資料載入作業。這個類別是抽象的,可做為所有載入器的基礎類別。您可以直接將 Loader 設為子類別,也可以使用下列任一內建子類別簡化實作程序:
|
以下各節將說明如何在應用程式中使用這些類別和介面。
在應用程式中使用載入器
本節說明如何在 Android 應用程式中使用載入器。使用載入器的應用程式通常包含以下內容:
FragmentActivity
或Fragment
。LoaderManager
的例項。- 用於載入
ContentProvider
備份資料的CursorLoader
。或者,您也可以實作自己的Loader
或AsyncTaskLoader
子類別,從其他來源載入資料。 LoaderManager.LoaderCallbacks
的實作。您可以在這裡建立新的載入器,以及管理現有載入器的參照。- 顯示載入器資料的方式,例如
SimpleCursorAdapter
。 - 使用
CursorLoader
時的資料來源,例如ContentProvider
。
啟動載入器
LoaderManager
會管理 FragmentActivity
或 Fragment
中的一或多個 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
) 預計會在停止後保留其資料。這樣做可讓應用程式在整個活動或片段的 onStop()
和 onStart()
方法中保留資料,讓使用者返回應用程式時,不必等候資料重新載入。
您會使用 LoaderManager.LoaderCallbacks
方法得知何時應建立新的載入器,以及何時應該停止使用載入器資料。
LoaderManager.LoaderCallbacks
包含以下方法:
onCreateLoader()
:為特定 ID 執行個體化並傳回新的Loader
。
-
onLoadFinished()
:在先前建立的載入器完成載入時呼叫。
onLoaderReset()
:在重設先前建立的載入器因故而無法使用資料時呼叫時呼叫。
這些方法會在以下各節中詳細說明。
onCreateLoader
當您嘗試透過 initLoader()
等載入器時,系統會檢查該 ID 指定的載入器是否存在。否則會觸發 LoaderManager.LoaderCallbacks
方法 onCreateLoader()
。您可以在這裡建立新的載入器。這通常是 CursorLoader
,但您可以實作自己的 Loader
子類別。
在以下範例中,onCreateLoader()
回呼方法使用其建構函式方法建立 CursorLoader
,該方法需要對 ContentProvider
執行查詢所需的完整資訊。具體而言,它需要下列項目:
- uri:要擷取的內容 URI。
- 投影:要傳回哪些資料欄的清單。傳送
null
會傳回所有無效的資料欄。 - selection:這個篩選器會宣告要傳回哪些資料列,格式為 SQL WHERE 子句 (不包括 WHERE 本身)。傳送
null
會傳回指定 URI 的所有資料列。 - selectionArgs:如果您在選取中加入 ?s,系統會按照選取項目在選取的順序,將 ?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
,其中包含聯絡人內容供應器的查詢結果。它會使用 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:示範如何使用節流功能,在內容供應器資料變更時減少查詢次數。