載入器

載入器已於 Android 9 (API 級別 28) 淘汰。針對 在處理 ActivityFragment 生命週期時,如何處理載入資料 ViewModel 物件的組合 和 LiveData。 檢視設定變更後仍然有效的模型,例如載入器 減少樣板程式碼LiveData 提供一種生命週期感知方式,載入可在其中重複使用的資料 多個檢視畫面模型您也可以使用以下項目合併 LiveDataMediatorLiveData。 任何可觀測的查詢,例如來自 Room 資料庫:可用來觀察變化 與資料相比

即使你沒有存取權,仍可使用「ViewModel」和「LiveData」 附加至 LoaderManager,例如 Service。將兩者 tandem 可讓您輕鬆存取應用程式需要的資料,而不必處理 UI 生命週期如要進一步瞭解LiveData,請參閱 LiveData總覽。如要進一步瞭解 ViewModel,請參閱「ViewModel 總覽」。

Loader API 可讓您從 內容供應器 或要顯示在 FragmentActivity 中的其他資料來源 或 Fragment

在沒有載入器的情況下,您可能會遇到以下問題:

  • 如果您直接透過活動或片段擷取資料,使用者 因執行可能速度緩慢而缺乏回應 從 UI 執行緒開始查詢
  • 如果您從其他執行緒 (可能使用 AsyncTask) 擷取資料, 則要共同管理 和 UI 執行緒,例如各種活動或片段的生命週期事件,例如 onDestroy() 和設定變更。

載入器解決這些問題並提供其他好處:

  • 載入器會在個別執行緒上執行,避免 UI 速度緩慢或沒有回應。
  • 載入器會在事件時提供回呼方法,藉此簡化執行緒管理 。
  • 載入器會在設定變更時保留結果並快取結果,防止 重複的查詢。
  • 載入器可以實作觀察器來監控基礎上的變更 做為資料來源例如,CursorLoader 會自動 註冊 ContentObserver 以觸發重新載入作業 資料變更時

載入器 API 摘要

使用應用程式時,可能會涉及多種類別和介面 應用程式中的載入器。下表摘要說明:

類別/介面 說明
LoaderManager FragmentActivityFragment管理一或多個帳戶 Loader 執行個體。只有一個 每個活動或片段 LoaderManager,但 LoaderManager 可管理多個載入器。

如要取得 LoaderManager,請呼叫 getSupportLoaderManager() 來自活動或片段

如要開始從載入器載入資料,請呼叫 initLoader()restartLoader()。 系統會自動判斷具有相同整數 ID 的載入程式是否已 並建立新的載入器,或重複使用現有的載入程式。

LoaderManager.LoaderCallbacks 此介麵包含 載入器事件。介面會定義三種回呼方法: ,瞭解如何調查及移除這項存取權。 您的活動或片段通常會實作這個介面, 在你撥打電話時登錄 initLoader()restartLoader()
Loader 載入器執行資料載入。這個類別為摘要 做為所有載入器的基礎類別您可以直接透過子類別 Loader 或使用下列其中一個內建函式 子類別簡化實作方式:

以下章節將說明如何使用這些設定 應用程式類別和介面

在應用程式中使用載入器

本節說明如何在 Android 應用程式中使用載入器。一個 使用載入器的應用程式通常包括:

啟動載入器

LoaderManager 管理 FragmentActivity 中的一或多個 Loader 執行個體,或 Fragment。每個活動或片段只有一個 LoaderManager

通常 在活動的 onCreate() 方法或片段的內初始化 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 保留相關資料如此一來,應用程式 不包含活動或片段 onStop()onStart() 方法的資料 使用者返回應用程式時,不必等待資料 重新載入。

您可以使用 LoaderManager.LoaderCallbacks 方法得知何時要建立新的載入器,並在載入器符合要求時通知應用程式 。

LoaderManager.LoaderCallbacks」包含以下福利 方法:

  • onLoaderReset(): 重設先前建立的載入程式時呼叫,導致其 資料不足。

以下各節將詳細說明這些方法。

onCreateLoader

當您嘗試存取載入器 (例如透過 initLoader()) 時,系統會檢查 由 ID 指定的載入器已存在。如果沒有,則會觸發 LoaderManager.LoaderCallbacks 方法 onCreateLoader()。這個 建立新的載入器這通常是 CursorLoader,但您可以自行實作 Loader 子類別。

在以下範例中,onCreateLoader() 回呼方法會使用其建構函式方法建立 CursorLoader, 需要完整的資訊集,才能對 ContentProvider 執行查詢。具體來說,這個檔案需要下列項目:

  • uri:要擷取內容的 URI。
  • 投影:要傳回哪些資料欄。傳球員 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

當先前建立的載入程式完成其載入時,系統就會呼叫此方法。 此方法保證在最後資料公布之前呼叫 您為這個載入程式提供的 屬性。此時,請移除 舊資料才會發布不發布資料 - 由載入器擁有並負責處理

載入器在瞭解應用程式已不存在後,就會釋出資料 利用 Vertex AI Workbench 使用者例如,如果資料是來自 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);
    }
}

其他示例

以下範例說明如何使用載入器: