Cargadores

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

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

La API de Loader permite cargar datos desde un proveedor de contenido o alguna otra fuente de datos para mostrar 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, tus usuarios Se trata de una falta de capacidad de respuesta debido a un rendimiento potencialmente lento consultas desde el subproceso de IU.
  • Si recuperas los datos de otro subproceso, quizás con AsyncTask, serás responsable de administrar el subproceso y el subproceso de IU a través de varios eventos de ciclo de vida de actividades o fragmentos, como onDestroy() y cambios de configuración.

Los cargadores resuelven estos problemas y ofrecen 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 subprocesos, ya que proporcionan métodos de devolución de llamada cuando se producen eventos. de que ocurran cambios.
  • Los cargadores persisten y almacenan en caché los resultados entre los cambios de configuración para evitar las consultas duplicadas.
  • Los cargadores pueden implementar un observador para supervisar los cambios en la configuración fuente de datos. Por ejemplo, CursorLoader automáticamente registra un ContentObserver para activar una recarga cuando cambian los datos.

Resumen de la API de Loader

Muchas clases e interfaces pueden participar al usar de las cargas de trabajo de una app. Se resumen en la siguiente tabla:

Clase/interfaz Descripción
LoaderManager Una clase abstracta asociada con un FragmentActivity o Fragment para administrar uno o más Loader instancias. Solo hay una LoaderManager por actividad o fragmento, pero un LoaderManager puede administrar varios cargadores.

Para obtener un LoaderManager, llama a getSupportLoaderManager(). de la actividad o el fragmento.

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

LoaderManager.LoaderCallbacks Esta interfaz contiene métodos de devolución de llamada que se llaman cuando loader. La interfaz define tres métodos de devolución de llamada:
  • onCreateLoader(int, Bundle): se llama cuando el sistema necesita que se cree un nuevo cargador. En tu código, crea un objeto Loader y muéstralo a el sistema.
  • onLoadFinished(Loader<D>, D): se llama cuando un cargador termina de cargar datos. Normalmente, mostrar los datos al usuario en tu código.
  • onLoaderReset(Loader<D>): se llama cuando se restablece un cargador previamente creado, cuando llamas destroyLoader(int) o cuando la actividad o fragmento se destruye y hace que sus datos no estén disponibles. En tu código, quitar cualquier referencia a los datos del cargador.
Por lo general, tu actividad o fragmento implementa esta interfaz, registrados cuando llamas initLoader() o restartLoader()
Loader Los cargadores cargan los datos. Esta clase es abstracta y sirve como la clase base para todos los cargadores. Puedes subclasificar directamente Loader o usa uno de los siguientes dispositivos integrados subclases para simplificar la implementación:

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

Usa cargadores en una aplicación

Esta sección describe cómo usar cargadores en una aplicación con Android. Los que usa cargadores, por lo general, incluye lo siguiente:

Inicia un cargador

LoaderManager administra una o más instancias de Loader en un FragmentActivity o Fragment Solo hay un LoaderManager por actividad o fragmento.

Normalmente, inicializamos un Loader dentro del método onCreate() de la actividad o en el onCreate(). Tú hazlo de la siguiente manera:

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 construcción (null en este ejemplo).
  • Una implementación de LoaderManager.LoaderCallbacks, que Las llamadas a LoaderManager para informar los 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 a initLoader() garantiza que un cargador esté inicializada y activa. Tiene dos resultados posibles:

  • Si el cargador especificado por el ID ya existe, el último cargador creado cuando se reutiliza.
  • Si el cargador especificado por el ID no existe, initLoader() activa la onCreateLoader() del método 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, el objeto LoaderManager.LoaderCallbacks especificado implementación se asocia con el cargador y se llama cuando el de estado del cargador de carga. Si, en el momento de esta llamada, el emisor se encuentra en su iniciado y el cargador solicitado ya existe y ha generado su 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 la vida del cargador automáticamente. El 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 la carga. cuando ocurren 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, usa un cargador existente con el ID especificado, si lo hay. Si no los hay, lo crea. Pero, a veces, quieres descartar datos antiguos y volver a empezar.

Para descartar tus datos anteriores, usa restartLoader(). Por ejemplo, el siguiente implementación de SearchView.OnQueryTextListener reinicios el cargador cuando la consulta del usuario cambia. El cargador debe reiniciarse para que que puede usar el filtro de búsqueda revisado para hacer 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 al cliente interactuar con LoaderManager.

Se espera que los cargadores, especialmente CursorLoader, retienen sus datos después de ser detenidos. Esto permite que las aplicaciones mantengan su datos en los métodos onStop() y onStart() de la actividad o el fragmento para que Cuando los usuarios regresan a una aplicación, no tienen que esperar a que los datos se volver a cargar la página.

Debes usar los métodos LoaderManager.LoaderCallbacks para saber cuándo crear un cargador nuevo y para indicarle a la aplicación cuándo está tiempo para dejar de usar los datos de un cargador.

LoaderManager.LoaderCallbacks incluye estos métodos:

  • onLoadFinished(): Se llama cuando termina de cargarse un cargador previamente creado.
  • onLoaderReset(): llamado cuando se restablece un cargador previamente creado, lo que hace que su datos no 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. De lo contrario, activa el método LoaderManager.LoaderCallbacks onCreateLoader(). Esta es donde creas un cargador nuevo. Por lo general, es una CursorLoader, pero puedes implementar tu propia subclase de Loader.

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

  • uri: Es el URI del contenido que se recuperará.
  • projection: una lista de qué columnas se deben mostrar. Aprobación null muestra todas las columnas, lo cual es ineficiente.
  • selection: Un filtro que declara qué filas mostrar formateado como una cláusula WHERE de SQL (excluyendo la propia WHERE). Aprobación null muestra todas las filas para el URI dado.
  • selectionArgs: si incluyes ?s en la selección, se reemplazan por los valores de selectionArgs en el orden en que aparecen en la selección. Los valores están vinculados como cadenas.
  • sortOrder: cómo ordenar las filas, con el formato de SQL ORDER BY (excepto la cláusula ORDER BY). Pasando por null usa el orden de clasificación predeterminado, que podría 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 proporciona para este cargador. En este punto, quita todo uso de los datos antiguos, ya que serán liberados. Pero no liberes los datos ya que el cargador es su propietario y se encarga de eso.

El cargador libera los datos una vez que sabe que la aplicación ya no está y la usan. Por ejemplo, si los datos son un cursor de una CursorLoader, no llames a close() en ella. Si el cursor se coloca colocado en un CursorAdapter, usa el método swapCursor() para que la El Cursor anterior no está cerrado, 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. Por lo tanto, lo que hace que sus datos no estén disponibles. Esta devolución de llamada te permite saber cuándo se a punto de liberarse para que puedas quitar la referencia a él.

Esta implementación requiere 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 que contiene los resultados de una consulta sobre el proveedor de contenido de contactos. Este usa un CursorLoader para administrar la consulta del proveedor.

Como este ejemplo es de una aplicación para acceder a los contactos de un usuario, 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: