Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

Cargadores

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, como onDestroy() 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 un ContentObserver 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 getSupportLoaderManager() desde la actividad o el fragmento.

Para comenzar a cargar datos desde un cargador, llama a initLoader() o a restartLoader(). El sistema determina automáticamente si ya existe un cargador con el mismo ID y crea un nuevo cargador o reutiliza un cargador existente.

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:
  • onCreateLoader(int, Bundle): Se lo llama cuando el sistema necesita que se cree un nuevo cargador. Tu código debería crear un objeto Loader y mostrárselo al sistema.
  • onLoadFinished(Loader<D>, D): Se lo llamada cuando un cargador termina de cargar datos. El código debería mostrarle los datos al usuario.
  • onLoaderReset(Loader<D>): Se lo llama cuando se reinicia un cargador que ya se había creado (cuando llamas a destroyLoader(int) o cuando finaliza la actividad o el fragmento, y, por lo tanto, sus datos no están disponibles). El código debería quitar cualquier tipo de referencia a los datos del cargador.
Por lo general, tu actividad o fragmento implementa la interfaz, que se genera cuando llamas a 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:

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 el LoaderManager para informar eventos del cargador. En este ejemplo, la clase local implementa la interfaz LoaderManager.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:

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:

  • 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: