載入器

載入器已於 Android 9 (API 級別 28) 淘汰。如要在處理 ActivityFragment 生命週期的同時載入資料,建議您使用 ViewModel 物件和 LiveData 的組合。載入模型等設定變更後,檢視畫面模型仍會保留,但樣板程式碼較少。LiveData 提供一種生命週期感知方式載入資料,可讓您在多個檢視模型中重複使用。您也可以使用 MediatorLiveData 合併 LiveData。任何可觀測的查詢 (例如 Room 資料庫中的查詢) 都可用來觀察資料的變更。

ViewModelLiveData 也適用於您無法存取 LoaderManager (例如在 Service 中) 的情況。同時使用這兩種模式可讓您輕鬆存取應用程式所需的資料,而無需處理 UI 生命週期。如要進一步瞭解 LiveData,請參閱「LiveData 總覽」。如要進一步瞭解 ViewModel,請參閱「ViewModel 總覽」。

Loader API 可讓您從內容供應器或其他資料來源載入資料,以便在 FragmentActivityFragment 中顯示。

如果沒有載入器,可能會遇到下列問題:

  • 如果您直接在活動或片段中擷取資料,使用者因為從 UI 執行緒執行查詢的速度可能過慢,導致回應速度不夠快。
  • 如果您從其他執行緒擷取資料 (可能使用 AsyncTask),則您必須負責透過各種活動或片段生命週期事件 (例如 onDestroy() 和設定變更) 管理該執行緒和 UI 執行緒。

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

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

Loader API 摘要

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

類別/介面 說明
LoaderManager FragmentActivityFragment 相關聯的抽象類別,用於管理一或多個 Loader 執行個體。每個活動或片段只有一個 LoaderManager,但 LoaderManager 可以管理多個載入器。

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

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

LoaderManager.LoaderCallbacks 此介麵包含載入器事件時呼叫的回呼方法。介面會定義三種回呼方法: 您的活動或片段通常會實作此介面,而且會在您呼叫 initLoader()restartLoader() 時註冊。
Loader 載入器會執行資料載入作業。這個類別是抽象的,可做為所有載入器的基礎類別。您可以直接將 Loader 設為子類別,也可以使用下列任一內建子類別簡化實作程序:

以下各節將說明如何在應用程式中使用這些類別和介面。

在應用程式中使用載入器

本節說明如何在 Android 應用程式中使用載入器。使用載入器的應用程式通常包含以下內容:

啟動載入器

LoaderManager 會管理 FragmentActivityFragment 中的一或多個 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 包含以下方法:

  • 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

如果重設先前建立的載入器,導致資料無法使用,系統會呼叫此方法。這個回呼可讓您瞭解資料即將釋出的時間,以便您移除對資料的參照。

此實作會呼叫值為 nullswapCursor()

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);
    }
}

其他示例

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