A partir de Android P (API 28), se dieron de baja los cargadores. La opción recomendada de administrar la carga de datos mientras se manejan los ciclos de vida de Activity y Fragment es usar una combinación de ViewModels
y LiveData
. Los ViewModels sobreviven a cambios de configuración como los Loaders, pero con menos código predeterminado. LiveData proporciona una forma de cargar los datos que puedes reutilizar en varios ViewModels. También puedes combinar LiveData utilizando MediatorLiveData
, y cualquier consulta observable, como las de Room database, puede utilizarse para observar los cambios en los datos. ViewModels y LiveData también están disponibles en situaciones en las que no tienes acceso a LoaderManager
, como en Service
. Usar los dos en tándem proporciona una manera fácil de acceder a los datos que tu aplicación necesita sin tener que lidiar con el ciclo de vida de la IU. Para obtener más información sobre LiveData, consulta la guía de LiveData; para obtener más información sobre ViewModels, visita la guía de ViewModel.
La API de Loader te permite cargar datos de un proveedor de contenido u otra fuente de datos en un FragmentActivity
o Fragment
. Si no entiendes por qué necesitas API de Loader para realizar esta operación aparentemente trivial, primero considera algunos de los problemas que podrías encontrar sin los cargadores:
- Si obtienes los datos directamente en la actividad o el fragmento, tus usuarios sufrirán la falta de respuesta debido a la realización de consultas potencialmente lentas desde el subproceso de la IU.
- Si obtienes los datos de otro subproceso, tal vez con
AsyncTask
, entonces eres responsable de administrar tanto ese subproceso como el de la IU a través de varias actividades o eventos del ciclo de vida de fragmentos, comoonDestroy()
y cambios de configuración.
Los cargadores resuelven estos problemas y ofrecen otras ventajas. Por ejemplo:
- Los cargadores se ejecutan en subprocesos separados para evitar que la IU esté en mal estado o no responda.
- Los cargadores simplifican la gestión de los subprocesos, ya que proporcionan métodos de devolución de llamada cuando se producen eventos.
- Los cargadores almacenan en caché los resultados en todos los cambios de configuración para evitar consultas duplicadas.
- Los cargadores pueden implementar un observador para supervisar los cambios en la fuente de datos subyacente. Por ejemplo,
CursorLoader
registra automáticamente unContentObserver
para activar una recarga cuando cambian los datos.
Resumen de la API de Loader
Muchas clases e interfaces pueden participar en el uso de cargadores en una aplicación. Se resumen en esta tabla:
Clase/interfaz | Descripción |
---|---|
LoaderManager |
Clase abstracta asociada con un FragmentActivity o un Fragment para administrar una o más instancias de Loader . Solo hay un LoaderManager por actividad o fragmento, pero un LoaderManager puede administrar varios cargadores.
Para obtener LoaderManager, llama a Para comenzar a cargar datos desde un cargador, llama a |
LoaderManager.LoaderCallbacks |
Esta interfaz contiene métodos de devolución de llamada que se llaman cuando ocurren eventos de cargadores. La interfaz define tres métodos de devolución de llamada:
initLoader() o a restartLoader() .
|
Loader |
Los cargadores cargan los datos. Esta clase es abstracta y funciona como la base de todos los cargadores. Puedes dividir directamente a Loader en una subclase o usar una de las siguientes subclases integradas para simplificar la implementación:
|
Las siguientes secciones muestran cómo usar estas clases e interfaces en una aplicación.
Uso de cargadores en una aplicación
Esta sección describe cómo usar cargadores en una aplicación con Android. Una aplicación que usa cargadores suele incluir lo siguiente:
- Un
FragmentActivity
o unFragment
. - Una instancia del
LoaderManager
. - Un
CursorLoader
para cargar datos con copia de seguridad por unContentProvider
. Además, puedes implementar tu propia subclase deLoader
oAsyncTaskLoader
para cargar datos de otra fuente. - Una implementación de
LoaderManager.LoaderCallbacks
. Aquí es donde creas cargadores nuevos y administras tus referencias a cargadores existentes. - Una manera de mostrar los datos del cargador, como un
SimpleCursorAdapter
. - Una fuente de datos, como un
ContentProvider
, cuando se usa unCursorLoader
.
Cómo iniciar un cargador
LoaderManager
administra una o más instancias de Loader
en un FragmentActivity
o unFragment
. Solo hay un LoaderManager
por actividad o fragmento.
Generalmente, un Loader
se inicia en el método onCreate()
de la actividad o dentro del método onActivityCreated()
del fragmento. Para ello, debes seguir estos pasos:
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);
El método initLoader()
adopta los siguientes parámetros:
- Un ID único que identifica el cargador. En este ejemplo, es 0.
- Argumentos opcionales para proporcionar al cargador en la creación (
null
en este ejemplo). - Una implementación de
LoaderManager.LoaderCallbacks
, a la que llama elLoaderManager
para informar eventos del cargador. En este ejemplo, la clase local implementa la interfazLoaderManager.LoaderCallbacks
, por lo que pasa una referencia a sí misma,this
.
La llamada initLoader()
garantiza que se inicie un cargador y que esté activo. Tiene dos resultados posibles:
- Si ya existe el cargador especificado por el ID, se reutiliza el último cargador creado.
- Si no existe,
initLoader()
activa un método deLoaderManager.LoaderCallbacks
:onCreateLoader()
Es aquí donde implementas el código para crear instancias y mostrar un nuevo cargador. Para obtener más información, consulta la sección onCreateLoader.
En ambos casos, la implementación de LoaderManager.LoaderCallbacks
está asociada con el cargador y se llamará cuando cambie el estado del cargador. Si, cuando se realiza esta llamada, el emisor está iniciándose y el cargador solicitado ya existe y ha generado sus datos, el sistema llama a onLoadFinished()
inmediatamente (durante initLoader()
). Por ello, debes estar preparado para que esto suceda. Para obtener más información sobre esta devolución de llamada, consulta onLoadFinished.
Ten en cuenta que el método initLoader()
muestra el Loader
que se crea, pero no necesitas capturar una referencia para él. LoaderManager
administra la vida del cargador automáticamente. LoaderManager
inicia y detiene la carga cuando es necesario, y mantiene el estado del cargador y el contenido asociado. Esto implica que rara vez interactúas con los cargadores directamente (no obstante, a fin de ver un ejemplo del uso de métodos para perfeccionar el comportamiento de un cargador, consulta el ejemplo LoaderThrottle). Utilizas con más frecuencia los métodos LoaderManager.LoaderCallbacks
para intervenir en el proceso de carga cuando se produce un evento particular. Para obtener más información sobre este tema, consulta Cómo usar las devoluciones de llamada de LoaderManager.
Cómo reiniciar un cargador
Cuando se usa initLoader()
, como se muestra arriba, este usa un cargador existente con el ID especificado (si hay uno). De lo contrario, lo crea. Sin embargo, a veces, quieres descartar datos obsoletos y comenzar de nuevo.
Para descartar esos datos, se usa restartLoader()
. Por ejemplo, esta implementación de SearchView.OnQueryTextListener
reinicia el cargador cuando cambia la consulta del usuario. El cargador se debe reiniciar para que pueda usar el filtro de búsqueda revisado a fin de realizar una consulta nueva.
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; }
Cómo usar las devoluciones de llamada de LoaderManager
LoaderManager.LoaderCallbacks
es una interfaz de devolución de llamada que permite que un cliente interactúe con el LoaderManager
.
Se espera que los cargadores, especialmente CursorLoader
, retengan los datos después de que se los haya detenido. Esto permite que las aplicaciones mantengan los datos en los métodos onStop()
y onStart()
de la actividad o el fragmento para que, cuando los usuarios vuelvan a la aplicación, no tengan que esperar que los datos vuelvan a cargarse. Los métodos LoaderManager.LoaderCallbacks
se utilizan para saber cuándo crear un cargador nuevo y para decir a la aplicación cuándo es el momento de detener el uso de los datos de un cargador.
LoaderManager.LoaderCallbacks
incluye estos métodos:
onCreateLoader()
: Crea instancias y muestra unLoader
nuevo para el ID indicado.
-
onLoadFinished()
: Se lo llama cuando termina de cargarse un cargador previamente creado.
onLoaderReset()
: Recibe una llamada durante el restablecimiento de un cargador previamente creado. Esto hace que los datos no estén disponibles.
Estos métodos se describen más detalladamente en las secciones siguientes.
onCreateLoader
Cuando intentas acceder a un cargador (por ejemplo, mediante initLoader()
), este comprueba si existe el cargador especificado por el ID. Si no existe, activa un método de LoaderManager.LoaderCallbacks
: onCreateLoader()
. Aquí es donde creas un cargador nuevo. Generalmente, será CursorLoader
, pero puedes implementar tu propia subclase de Loader
.
En este ejemplo, el método de devolución de llamada onCreateLoader()
crea un CursorLoader
. Debes crear el CursorLoader
con tu método constructor, que requiere el conjunto completo de información necesario para realizar una consulta a ContentProvider
. Específicamente, necesita lo siguiente:
- uri: El URI del contenido que se debe recuperar.
- projection: Una lista de qué columnas se deben mostrar. Si se pasa
null
, se mostrarán todas las columnas, lo que es ineficiente. - selection: Un filtro que declara qué filas se deben mostrar, con el formato de una cláusula WHERE de SQL (se excluye la expresión "WHERE"). Si se pasa
null
, se mostrarán todas las filas del URI específico. - selectionArgs: Puedes incluir "?s" en la selección, que se reemplazará por los valores de selectionArgs, en el orden en el que aparezcan en la selección. Los valores se enlazarán como strings.
- sortOrder: Cómo ordenar las filas con el formato de una cláusula ORDER BY de SQL (se excluye la expresión "ORDER BY"). Si se pasa
null
, se usará el orden de organización predeterminado, que puede no tener un orden.
Por ejemplo:
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
Se lo llama cuando termina de cargarse un cargador previamente creado. Este método se llama antes de que se liberen los últimos datos que se proporcionaron para este cargador. En este punto, debes quitar todo uso de los datos obsoletos (ya que se liberarán pronto), pero no debes hacerlo por tu cuenta, ya que el cargador es su propietario y se encargará de eso.
El cargador liberará los datos una vez que detecte que la aplicación ya no los usa. Por ejemplo, si los datos representan un cursor de un CursorLoader
, no debes llamar a close()
en él tú mismo. Si el cursor se dispone en un CursorAdapter
, debes usar el método swapCursor()
para que el Cursor
anterior no se cierre. Por ejemplo:
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
Este método recibe una llamada durante el restablecimiento de un cargador previamente creado. Esto hace que los datos no estén disponibles. Esta devolución de llamada te permite saber cuándo se liberarán los datos para que puedas quitar la referencia a ellos.
Esta implementación llama a swapCursor()
con un valor 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); }
Ejemplo
A modo de ejemplo, la siguiente es la implementación completa de un Fragment
que muestra una ListView
con los resultados de una consulta de los contactos del proveedor de contenido. Usa un CursorLoader
para manejar la consulta sobre el proveedor.
Para que una aplicación acceda a los contactos de un usuario, como se muestra en este ejemplo, el manifiesto debe incluir el permiso 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 onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(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 // Prepare the loader. Either re-connect with an existing one, // or start a new one. loaderManager.initLoader(0, null, this) } 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 onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(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); // Prepare the loader. Either re-connect with an existing one, // or start a new one. getLoaderManager().initLoader(0, null, this); } @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); } }
Más ejemplos
Los siguientes ejemplos indican cómo usar cargadores:
- LoaderCursor: Una versión completa del fragmento de código que se muestra arriba.
- Cómo recuperar una lista de contactos: Una explicación que usa un
CursorLoader
para recuperar datos del proveedor de contactos. - LoaderThrottle: Un ejemplo de cómo usar la limitación para reducir la cantidad de consultas que hace un proveedor de contenido cuando cambian los datos.
AsyncTaskLoader
: Ejemplo que usa unAsyncTaskLoader
para cargar las apps instaladas del administrador de paquetes.