加载器

从 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,请调用 getSupportLoaderManager() 从 activity 或 fragment 中获取。

如需开始从加载器加载数据,请调用 initLoader() 或 restartLoader()。 系统会自动确定具有相同整数 ID 的加载器是否已 并创建一个新加载器或重复使用现有加载器。

LoaderManager.LoaderCallbacks 该接口包含回调方法, 加载程序事件发生时抛出的异常。该接口定义了三种回调方法: <ph type="x-smartling-placeholder">。 您的 activity 或 fragment 通常会实现此接口,并且 您在调用 initLoader() 或 restartLoader()。
Loader 加载器用于加载数据。这个类是抽象类, 作为所有加载器的基类。您可以直接创建子类 Loader 或使用以下任一内置功能 子类以简化实现: <ph type="x-smartling-placeholder">

以下部分将介绍如何使用 类和接口

在应用中使用加载器

本部分介绍了如何在 Android 应用中使用加载器。一个 使用加载器的应用通常包含以下内容:

启动加载器

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包含以下内容 方法:

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

更多示例

以下示例说明如何使用加载器: