Caricatori

I caricatori sono stati ritirati a partire da Android 9 (livello API 28). L'opzione consigliata per gestire il caricamento dei dati durante la gestione dei cicli di vita di Activity e Fragment è utilizzare una combinazione di oggetti ViewModel e LiveData. I modelli di visualizzazione sopravvivono alle modifiche alla configurazione, ad esempio i caricatori, ma con meno codice boilerplate. LiveData fornisce un metodo di caricamento dei dati basato sul ciclo di vita che puoi riutilizzare in più modelli di vista. Puoi anche combinare LiveData utilizzando MediatorLiveData. Qualsiasi query osservabile, come quelle di un database delle camere, può essere utilizzata per osservare le modifiche ai dati.

ViewModel e LiveData sono disponibili anche in situazioni in cui non hai accesso a LoaderManager, ad esempio in Service. L'utilizzo di entrambi in tandem offre un modo semplice per accedere ai dati di cui la tua app ha bisogno senza dover gestire il ciclo di vita dell'interfaccia utente. Per ulteriori informazioni su LiveData, consulta la panoramica di LiveData. Per scoprire di più su ViewModel, consulta la panoramica di ViewModel.

L'API Loader ti consente di caricare i dati da un fornitore di contenuti o da un'altra origine dati per visualizzarli in un FragmentActivity o Fragment.

Senza i caricatori, potresti riscontrare quanto segue:

  • Se recuperi i dati direttamente nell'attività o nel frammento, gli utenti soffrono di scarsa reattività dovuta all'esecuzione di query potenzialmente lente dal thread dell'interfaccia utente.
  • Se recuperi i dati da un altro thread, magari con AsyncTask, sei responsabile della gestione sia del thread sia del thread dell'interfaccia utente tramite vari eventi del ciclo di vita delle attività o dei frammenti, come onDestroy() e modifiche alla configurazione.

I caricatori risolvono questi problemi e offrono altri vantaggi:

  • I caricatori vengono eseguiti su thread separati per evitare un'interfaccia utente lenta o non reattiva.
  • I caricatori semplificano la gestione dei thread fornendo metodi di callback quando si verificano eventi.
  • I caricatori vengono mantenuti e memorizzano nella cache i risultati durante le modifiche alla configurazione per evitare query duplicate.
  • I caricatori possono implementare un osservatore per monitorare le modifiche all'origine dati sottostante. Ad esempio, CursorLoader registra automaticamente una richiesta ContentObserver per attivare un ricaricamento quando i dati cambiano.

Riepilogo API Loader

Quando si utilizzano i caricatori in un'app, possono essere coinvolte più classi e interfacce che sono riassunte nella seguente tabella:

Lezione/interfaccia Descrizione
LoaderManager Una classe astratta associata a FragmentActivity o Fragment per la gestione di una o più istanze Loader. È presente un solo LoaderManager per attività o frammento, ma un LoaderManager può gestire più caricatori.

Per ottenere un LoaderManager, chiama getSupportLoaderManager() dall'attività o dal frammento.

Per iniziare a caricare i dati da un caricatore, chiama initLoader() o restartLoader(). Il sistema determina automaticamente se esiste già un caricatore con lo stesso ID intero e crea un nuovo caricatore o riutilizza un caricatore esistente.

LoaderManager.LoaderCallbacks Questa interfaccia contiene metodi di callback che vengono chiamati quando si verificano gli eventi del caricatore. L'interfaccia definisce tre metodi di callback:
  • onCreateLoader(int, Bundle): richiamato quando il sistema necessita di creazione di un nuovo caricatore. Nel codice, crea un oggetto Loader e restituiscelo al sistema.
  • onLoadFinished(Loader<D>, D): viene richiamato quando un caricatore ha terminato il caricamento dei dati. In genere i dati vengono mostrati all'utente nel codice.
  • onLoaderReset(Loader<D>): richiamato quando un caricatore creato in precedenza viene reimpostato, quando chiami destroyLoader(int) o quando l'attività o il frammento vengono eliminati, rendendo i suoi dati non disponibili. Nel codice, rimuovi qualsiasi riferimento ai dati del caricatore.
Solitamente l'attività o il frammento implementa questa interfaccia e viene registrato quando chiami initLoader() o restartLoader().
Loader I caricatori eseguono il caricamento dei dati. Questa classe è astratta e funge da base per tutti i caricatori. Per semplificare l'implementazione, puoi utilizzare direttamente una sottoclasse Loader o utilizzare una delle seguenti sottoclassi integrate:

Le sezioni seguenti mostrano come utilizzare queste classi e interfacce in un'applicazione.

Utilizzare i caricatori in un'applicazione

Questa sezione descrive come utilizzare i caricatori in un'applicazione Android. Un'applicazione che utilizza caricatori in genere include quanto segue:

Avvia un caricatore

LoaderManager gestisce una o più istanze Loader all'interno di un FragmentActivity o Fragment. Esiste un solo LoaderManager per attività o frammento.

In genere, devi inizializzare Loader all'interno del metodo onCreate() dell'attività o del metodo onCreate() del frammento. Procedi nel seguente modo:

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

Il metodo initLoader() accetta i seguenti parametri:

  • Un ID univoco che identifica il caricatore. In questo esempio, l'ID è 0.
  • Argomenti facoltativi da fornire al caricatore durante la creazione (null in questo esempio).
  • Un'implementazione LoaderManager.LoaderCallbacks, che LoaderManager chiama per segnalare gli eventi del caricatore. In questo esempio, la classe locale implementa l'interfaccia LoaderManager.LoaderCallbacks, quindi passa un riferimento a se stessa, this.

La chiamata initLoader() assicura che un caricatore sia inizializzato e attivo. Ci sono due possibili risultati:

  • Se il caricatore specificato dall'ID esiste già, viene riutilizzato l'ultimo caricatore creato.
  • Se il caricatore specificato dall'ID non esiste, initLoader() attiva il metodo LoaderManager.LoaderCallbacks onCreateLoader(). È qui che implementi il codice per creare un'istanza e restituire un nuovo caricatore. Per ulteriori informazioni, consulta la sezione relativa a onCreateLoader.

In entrambi i casi, l'implementazione LoaderManager.LoaderCallbacks specificata è associata al caricatore e viene richiamata quando cambia lo stato del caricatore. Se, al momento della chiamata, il chiamante è nello stato di avvio e il caricatore richiesto esiste già e ha generato i dati, il sistema chiama immediatamente onLoadFinished() durante initLoader(). È necessario essere preparati affinché ciò accada. Per ulteriori informazioni su questo callback, consulta la sezione su onLoadFinished.

Il metodo initLoader() restituisce il valore Loader creato, ma non è necessario acquisirne un riferimento. Il ciclo di vita del caricatore viene gestito automaticamente da LoaderManager. LoaderManager avvia e interrompe il caricamento quando necessario e mantiene lo stato del caricatore e dei relativi contenuti associati.

Ciò significa che raramente si interagisce direttamente con i caricatori. Di solito utilizzi i metodi LoaderManager.LoaderCallbacks per intervenire nel processo di caricamento quando si verificano determinati eventi. Per ulteriori informazioni su questo argomento, consulta la sezione Utilizzo dei callback di LoaderManager.

Riavviare un caricatore

Quando utilizzi initLoader(), come mostrato nella sezione precedente, viene utilizzato un caricatore esistente con l'ID specificato, se presente. In caso contrario, ne viene creato uno. A volte però potreste voler eliminare i vecchi dati e ricominciare da capo.

Per eliminare i dati precedenti, utilizza restartLoader(). Ad esempio, la seguente implementazione di SearchView.OnQueryTextListener riavvia il caricatore quando la query dell'utente cambia. È necessario riavviare il caricatore per utilizzare il filtro di ricerca rivisto per effettuare una nuova query.

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

Utilizzare i callback di LoaderManager

LoaderManager.LoaderCallbacks è un'interfaccia di callback che consente a un client di interagire con LoaderManager.

I caricatori, in particolare CursorLoader, dovrebbero conservare i dati dopo l'arresto. In questo modo le applicazioni possono conservare i propri dati nei metodi onStop() e onStart() dell'attività o del frammento, in modo che, quando gli utenti tornano a un'applicazione, non debbano attendere il ricaricamento dei dati.

Puoi usare i metodi LoaderManager.LoaderCallbacks per sapere quando creare un nuovo caricatore e per comunicare all'applicazione quando è il momento di interrompere l'utilizzo dei dati di un caricatore.

LoaderManager.LoaderCallbacks include questi metodi:

  • onLoadFinished(): viene richiamato quando un caricatore creato in precedenza ha terminato il caricamento.
  • onLoaderReset(): viene richiamato quando un caricatore creato in precedenza viene reimpostato, rendendo i suoi dati non disponibili.

Questi metodi sono descritti in maggiore dettaglio nelle sezioni seguenti.

onCreateLoader

Quando tenti di accedere a un caricatore, ad esempio tramite initLoader(), il programma verifica se il caricatore specificato dall'ID esiste. In caso contrario, attiva il metodo LoaderManager.LoaderCallbacks onCreateLoader(). Qui puoi creare un nuovo caricatore. In genere si tratta di una classe CursorLoader, ma puoi implementare la tua sottoclasse Loader.

Nell'esempio seguente, il metodo di callback onCreateLoader() crea una CursorLoader utilizzando il proprio metodo del costruttore, che richiede il set completo di informazioni necessarie per eseguire una query sul ContentProvider. Nello specifico:

  • uri: l'URI dei contenuti da recuperare.
  • projection: un elenco delle colonne da restituire. Superare null restituisce tutte le colonne, il che non è efficace.
  • selection: un filtro che dichiara quali righe restituire, formattato come clausola SQL WHERE (esclusa la clausola WHERE). Se passi null, vengono restituite tutte le righe dell'URI specificato.
  • selectionArgs: se includi ?s nella selezione, questi vengono sostituiti dai valori di selectionArgs nell'ordine in cui appaiono nella selezione. I valori vengono associati come stringhe.
  • sortOrder: come ordinare le righe, formattate come clausola SQL ORDER BY (esclusa la stessa clausola ORDER BY). Se viene superato null, viene utilizzato l'ordinamento predefinito, che potrebbe essere annullato.

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

Questo metodo viene richiamato quando un caricatore creato in precedenza termina il caricamento. Questo metodo verrà chiamato prima del rilascio degli ultimi dati forniti per il caricatore. A questo punto, rimuovi tutti gli utilizzi dei vecchi dati, poiché stanno per essere rilasciati. Ma non rilasciare i dati manualmente, perché il caricatore li possiede e se ne occuperà.

Il caricatore rilascia i dati quando sa che l'applicazione non li utilizza più. Ad esempio, se i dati sono un cursore da un CursorLoader, non chiamare close() su di esso. Se il cursore viene posizionato in un elemento CursorAdapter, utilizza il metodo swapCursor() per fare in modo che la versione precedente di Cursor non venga chiusa, come mostrato nell'esempio seguente:

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

Questo metodo viene richiamato quando un caricatore creato in precedenza viene reimpostato, rendendo quindi i suoi dati non disponibili. Questo callback consente di scoprire quando i dati stanno per essere pubblicati, in modo da poter rimuovere il riferimento a essi.

Questa implementazione chiama swapCursor() con un valore di 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);
}

Esempio

Ad esempio, ecco l'implementazione completa di un elemento Fragment che mostra un elemento ListView contenente i risultati di una query sul fornitore di contenuti dei contatti. Utilizza un CursorLoader per gestire la query sul provider.

Poiché questo esempio riguarda un'applicazione per accedere ai contatti di un utente, il manifest deve includere l'autorizzazione 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);
    }
}

Altri esempi

I seguenti esempi illustrano come utilizzare i caricatori:

  • LoaderCursor: una versione completa dello snippet precedente.
  • Recupera un elenco di contatti: una procedura dettagliata che utilizza CursorLoader per recuperare i dati dal fornitore di contatti.
  • LoaderThrottle: un esempio di come utilizzare la limitazione per ridurre il numero di query eseguite da un fornitore di contenuti quando i suoi dati cambiano.