Cargadores

Los cargadores dejaron de estar disponibles a partir de Android 9 (nivel de API 28). La opción recomendada para manejar la carga de datos mientras se controlan los ciclos de vida de Activity y Fragment es usar una combinación de objetos ViewModel y LiveData. Los modelos de vista sobreviven a cambios de configuración, como los cargadores, pero con menos código estándar. LiveData proporciona una forma optimizada para los ciclos de vida de cargar datos que puedes volver a usar en varios modelos de vistas. También puedes combinar LiveData usando MediatorLiveData. Cualquier búsqueda observable, como las de una base de datos de Room, se puede usar para observar cambios en los datos.

ViewModel y LiveData también están disponibles en situaciones en las que no tienes acceso a LoaderManager, como en un Service. Usar los dos en conjunto proporciona una manera fácil de acceder a los datos que necesita tu app sin tener que lidiar con el ciclo de vida de la IU. Para obtener más información sobre LiveData, consulta la descripción general de LiveData. Para obtener más información sobre ViewModel, consulta la descripción general de ViewModel.

La API de Loader te permite cargar datos desde un proveedor de contenido u otra fuente de datos para mostrarlos en un FragmentActivity o Fragment.

Sin cargadores, algunos de los problemas que puedes encontrar incluyen los siguientes:

  • Si recuperas los datos directamente en la actividad o el fragmento, los usuarios se verán afectados por la falta de respuesta debido a que realizan consultas potencialmente lentas desde el subproceso de IU.
  • Si recuperas los datos de otro subproceso, tal vez con AsyncTask, entonces eres responsable de administrar ese subproceso y el de la IU mediante diversos eventos de ciclo de vida de actividades o fragmentos, como onDestroy() y los cambios de configuración.

Los cargadores resuelven estos problemas e incluyen otros beneficios:

  • Los cargadores se ejecutan en subprocesos separados para evitar una IU lenta o que no responde.
  • Los cargadores simplifican la administración de los subprocesos, ya que proporcionan métodos de devolución de llamada cuando se producen eventos.
  • Los cargadores conservan y 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

Hay varias interfaces y clases que pueden participar en el uso de cargadores en una app. Se resumen en la siguiente tabla:

Clase/interfaz Descripción
LoaderManager Una 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 un LoaderManager, llama a getSupportLoaderManager() desde la actividad o el fragmento.

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

LoaderManager.LoaderCallbacks Esta interfaz contiene métodos de devolución de llamada a los que se llama cuando ocurren eventos del cargador. 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 cargador nuevo. En tu código, crea un objeto Loader y muéstralo al sistema.
  • onLoadFinished(Loader<D>, D): Se lo llama cuando un cargador termina de cargar datos. Por lo general, muestras los datos al usuario en tu código.
  • onLoaderReset(Loader<D>): Se lo llama cuando se restablece un cargador previamente creado, cuando llamas a destroyLoader(int) o cuando se destruye la actividad o el fragmento, lo que hace que los datos no estén disponibles. En tu código, quita cualquier referencia a los datos del cargador.
Por lo general, tu actividad o fragmento implementa esta interfaz, y se registra cuando llamas a initLoader() o restartLoader().
Loader Los cargadores cargan los datos. Esta clase es abstracta y funciona como la clase base para todos los cargadores. Puedes subclasificar Loader directamente o usar una de las siguientes subclases integradas para simplificar la implementación:

En las siguientes secciones, se muestra cómo usar estas interfaces y clases en una aplicación.

Usa 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 dentro de FragmentActivity o Fragment. Solo hay un LoaderManager por actividad o fragmento.

Por lo general, inicializas un Loader en el método onCreate() de la actividad o en el método onCreate() del fragmento. Para ello, sigue 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() toma los siguientes parámetros:

  • Un ID único que identifica el cargador. En este ejemplo, el ID 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 LoaderManager llama 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 un cargador se inicialice y esté activo. Tiene dos resultados posibles:

  • Si el cargador especificado por el ID ya existe, se reutiliza el último cargador creado.
  • Si el cargador especificado por el ID no existe, initLoader() activa el método onCreateLoader() de LoaderManager.LoaderCallbacks. Aquí es donde implementas el código para crear una instancia y mostrar un cargador nuevo. Para obtener más información, consulta la sección sobre onCreateLoader.

En cualquier caso, la implementación de LoaderManager.LoaderCallbacks determinada está asociada con el cargador y se llama cuando cambia el estado del cargador. Si, en el punto de esta llamada, el llamador está en estado iniciado y el cargador solicitado ya existe y generó sus datos, el sistema llama a onLoadFinished() de inmediato, durante initLoader(). Debes estar preparado para que esto suceda. Para obtener más información sobre esta devolución de llamada, consulta la sección sobre onLoadFinished.

El método initLoader() muestra el Loader que se crea, pero no es necesario capturar una referencia a él. LoaderManager administra automáticamente la vida del cargador. 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. Por lo general, usas los métodos LoaderManager.LoaderCallbacks para intervenir en el proceso de carga cuando se producen eventos particulares. Para obtener más información sobre este tema, consulta la sección Cómo usar las devoluciones de llamada de LoaderManager.

Cómo reiniciar un cargador

Cuando usas initLoader(), como se muestra en la sección anterior, utiliza un cargador existente con el ID especificado, si lo hay. Si no hay, crea una. Pero, a veces, quieres descartar datos antiguos y volver a empezar.

Para descartar datos antiguos, usa restartLoader(). Por ejemplo, la siguiente 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 a un cliente interactuar con LoaderManager.

Se espera que los cargadores, en particular CursorLoader, retengan sus datos después de que se detengan. Esto permite que las aplicaciones conserven sus datos en los métodos onStop() y onStart() de la actividad o el fragmento, de modo que, cuando los usuarios regresen a una aplicación, no tengan que esperar a que se vuelvan a cargar los datos.

Los métodos LoaderManager.LoaderCallbacks se usan para saber cuándo crear un cargador nuevo y para indicarle a la aplicación cuándo es el momento de dejar de usar los datos de un cargador.

LoaderManager.LoaderCallbacks incluye estos métodos:

  • onLoadFinished(): Se lo llama cuando un cargador previamente creado termina de cargarse.
  • onLoaderReset(): Se lo llama cuando se restablece 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, a través de initLoader(), este comprueba si existe el cargador especificado por el ID. Si no es así, activa el método LoaderManager.LoaderCallbacks onCreateLoader(). Aquí es donde creas un cargador nuevo. Por lo general, es una CursorLoader, pero puedes implementar tu propia subclase Loader.

En el siguiente ejemplo, el método de devolución de llamada onCreateLoader() crea un CursorLoader usando su método de constructor, que requiere el conjunto completo de información necesaria para realizar una consulta a ContentProvider. En particular, necesita lo siguiente:

  • uri: Es el URI del contenido que se recuperará.
  • projection: Una lista de las columnas que se mostrarán. Si pasas null, se mostrarán todas las columnas, lo que es ineficiente.
  • selection: un filtro que declara qué filas mostrar, con el formato de una cláusula WHERE de SQL (se excluye la expresión WHERE). Si pasas null, se muestran todas las filas del URI dado.
  • selectionArgs: Si incluyes "?s" en la selección, se reemplazarán con los valores de selectionArgs en el orden en el que aparezcan en la selección. Los valores están vinculados como cadenas.
  • sortOrder: cómo ordenar las filas, con el formato de una cláusula ORDER BY de SQL (se excluye la expresión ORDER BY) Cuando pasas null, se usa el orden de clasificación predeterminado, que puede no estar ordenado.

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 llama a este método cuando un cargador previamente creado termina su carga. Se garantiza que se llamará a este método antes de liberar los últimos datos que se proporcionen a este cargador. En este punto, quita todo uso de los datos antiguos, ya que se lanzarán. Sin embargo, no liberes los datos por tu cuenta, ya que el cargador es su propietario y se encarga de eso.

El cargador libera los datos una vez que detecta que la aplicación ya no los usa. Por ejemplo, si los datos corresponden a un cursor de un CursorLoader, no llames a close() en él tú mismo. Si el cursor se coloca en un CursorAdapter, usa el método swapCursor() para que el Cursor anterior no se cierre, como se muestra en el siguiente 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

Se llama a este método cuando se restablece un cargador previamente creado, lo que 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 de 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, esta es la implementación completa de un Fragment que muestra un ListView con los resultados de una búsqueda en el proveedor de contenido de los contactos. Usa un CursorLoader para administrar la consulta en el proveedor.

Debido a que este ejemplo proviene de una aplicación para acceder a los contactos de un usuario, su 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 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);
    }
}

Más ejemplos

Los siguientes ejemplos indican cómo usar cargadores: