Chargeurs

Les chargeurs sont obsolètes depuis Android 9 (niveau d'API 28). L'option recommandée pour gérer le chargement des données lors de la gestion des cycles de vie Activity et Fragment consiste à utiliser une combinaison d'objets ViewModel et LiveData. Les modèles de vue survivent aux modifications de configuration, comme les chargeurs, mais avec moins de code récurrent. LiveData fournit un moyen sensible au cycle de vie de charger des données que vous pouvez réutiliser dans plusieurs modèles de vue. Vous pouvez également combiner LiveData à l'aide de MediatorLiveData. Toutes les requêtes observables, telles que celles provenant d'une base de données Room, peuvent être utilisées pour observer les modifications apportées aux données.

ViewModel et LiveData sont également disponibles si vous n'avez pas accès à LoaderManager, par exemple dans un Service. L'utilisation des deux en tandem permet d'accéder facilement aux données dont votre application a besoin sans avoir à gérer le cycle de vie de l'UI. Pour en savoir plus sur LiveData, consultez la présentation de LiveData. Pour en savoir plus sur ViewModel, consultez la présentation de ViewModel.

L'API Loader vous permet de charger des données à partir d'un fournisseur de contenu ou d'une autre source de données pour les afficher dans un FragmentActivity ou un Fragment.

Sans chargeurs, vous pourriez rencontrer certains des problèmes suivants:

  • Si vous récupérez les données directement dans l'activité ou le fragment, vos utilisateurs souffrent d'un manque de réactivité en raison de l'exécution de requêtes potentiellement lentes à partir du thread UI.
  • Si vous récupérez les données d'un autre thread, peut-être avec AsyncTask, vous êtes responsable de la gestion de ce thread et du thread UI via divers événements de cycle de vie d'activité ou de fragment, tels que onDestroy() et les modifications de configuration.

Les chargeurs résolvent ces problèmes et offrent d'autres avantages:

  • Les chargeurs s'exécutent sur des threads distincts pour éviter que l'interface utilisateur soit lente ou ne répond pas.
  • Les chargeurs simplifient la gestion des threads en fournissant des méthodes de rappel lorsque des événements se produisent.
  • Les chargeurs conservent et mettent en cache les résultats en cas de modification de la configuration pour éviter les requêtes en double.
  • Les chargeurs peuvent implémenter un observateur pour surveiller les modifications apportées à la source de données sous-jacente. Par exemple, CursorLoader enregistre automatiquement un ContentObserver pour déclencher une actualisation lorsque les données changent.

Récapitulatif de l'API Loader

Plusieurs classes et interfaces peuvent être impliquées lorsque vous utilisez des chargeurs dans une application. Elles sont résumées dans le tableau suivant:

Classe/Interface Description
LoaderManager Classe abstraite associée à un FragmentActivity ou à un Fragment pour gérer une ou plusieurs instances Loader. Il n'existe qu'un seul LoaderManager par activité ou fragment, mais un LoaderManager peut gérer plusieurs chargeurs.

Pour obtenir un LoaderManager, appelez getSupportLoaderManager() à partir de l'activité ou du fragment.

Pour commencer à charger des données à partir d'un chargeur, appelez initLoader() ou restartLoader(). Le système détermine automatiquement s'il existe déjà un chargeur ayant le même ID d'entier, puis crée un chargeur ou réutilise un chargeur existant.

LoaderManager.LoaderCallbacks Cette interface contient des méthodes de rappel qui sont appelées lorsque des événements du chargeur se produisent. L'interface définit trois méthodes de rappel :
  • onCreateLoader(int, Bundle) : appelé lorsqu'un système a besoin de créer un chargeur. Dans votre code, créez un objet Loader et renvoyez-le au système.
  • onLoadFinished(Loader<D>, D) : appelé lorsqu'un chargeur a fini de charger des données. Vous présentez généralement les données à l'utilisateur dans votre code.
  • onLoaderReset(Loader<D>) : appelé lorsqu'un chargeur créé précédemment est en cours de réinitialisation, lorsque vous appelez destroyLoader(int) ou lorsque l'activité ou le fragment sont détruits, ce qui rend ses données indisponibles. Dans votre code, supprimez toutes les références aux données du chargeur.
Votre activité ou fragment implémente généralement cette interface, et est enregistré lorsque vous appelez initLoader() ou restartLoader().
Loader Les chargeurs effectuent le chargement des données. Cette classe est abstraite et sert de classe de base pour tous les chargeurs. Vous pouvez directement sous-classer Loader ou utiliser l'une des sous-classes intégrées suivantes pour simplifier l'implémentation :

Les sections suivantes expliquent comment utiliser ces classes et interfaces dans une application.

Utiliser des chargeurs dans une application

Cette section explique comment utiliser des chargeurs dans une application Android. Une application qui utilise des chargeurs inclut généralement les éléments suivants:

Démarrer un chargeur

Le LoaderManager gère une ou plusieurs instances Loader dans un élément FragmentActivity ou Fragment. Il n'y a qu'un seul LoaderManager par activité ou fragment.

Vous initialisez généralement un Loader dans la méthode onCreate() de l'activité ou la méthode onCreate() du fragment. Pour ce faire, procédez comme suit:

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

La méthode initLoader() utilise les paramètres suivants:

  • Identifiant unique du chargeur. Dans cet exemple, l'ID est 0.
  • Arguments facultatifs à fournir au chargeur lors de la construction (null dans cet exemple).
  • Une implémentation de LoaderManager.LoaderCallbacks, que LoaderManager appelle pour signaler les événements du chargeur. Dans cet exemple, la classe locale implémente l'interface LoaderManager.LoaderCallbacks afin de transmettre une référence à elle-même, this.

L'appel initLoader() garantit qu'un chargeur est initialisé et actif. Deux résultats sont possibles:

  • Si le chargeur spécifié par l'ID existe déjà, le dernier chargeur créé est réutilisé.
  • Si le chargeur spécifié par l'ID n'existe pas, initLoader() déclenche la méthode LoaderManager.LoaderCallbacks onCreateLoader(). C'est ici que vous implémentez le code pour instancier et renvoyer un nouveau chargeur. Pour en savoir plus, consultez la section sur onCreateLoader.

Dans les deux cas, l'implémentation LoaderManager.LoaderCallbacks donnée est associée au chargeur et est appelée lorsque l'état du chargeur change. Si, au moment de cet appel, l'appelant est à l'état démarré et que le chargeur demandé existe déjà et a généré ses données, le système appelle onLoadFinished() immédiatement, pendant initLoader(). Vous devez vous y préparer. Pour en savoir plus sur ce rappel, consultez la section sur onLoadFinished.

La méthode initLoader() renvoie le Loader créé, mais vous n'avez pas besoin de capturer une référence à celui-ci. LoaderManager gère automatiquement la durée de vie du chargeur. LoaderManager démarre et arrête le chargement si nécessaire, et conserve l'état du chargeur et de son contenu associé.

Comme cela l'implique, vous interagissez rarement directement avec les chargeurs. Le plus souvent, les méthodes LoaderManager.LoaderCallbacks vous permettent d'intervenir dans le processus de chargement lorsque des événements particuliers se produisent. Pour en savoir plus à ce sujet, consultez la section Utiliser les rappels LoaderManager.

Redémarrer un chargeur

Lorsque vous utilisez initLoader(), comme indiqué dans la section précédente, il utilise un chargeur existant avec l'ID spécifié, le cas échéant. S'il n'y en a pas, cela en crée un. Mais parfois, vous voulez supprimer vos anciennes données et recommencer.

Pour supprimer vos anciennes données, utilisez restartLoader(). Par exemple, l'implémentation suivante de SearchView.OnQueryTextListener redémarre le chargeur lorsque la requête de l'utilisateur change. Le chargeur doit être redémarré afin de pouvoir utiliser le filtre de recherche révisé pour effectuer une nouvelle requête.

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

Utiliser les rappels LoaderManager

LoaderManager.LoaderCallbacks est une interface de rappel qui permet à un client d'interagir avec LoaderManager.

Les chargeurs, en particulier CursorLoader, doivent conserver leurs données après avoir été arrêtés. Cela permet aux applications de conserver leurs données dans les méthodes onStop() et onStart() de l'activité ou du fragment, de sorte que lorsque les utilisateurs reviennent dans une application, ils n'ont pas besoin d'attendre que les données soient actualisées.

Les méthodes LoaderManager.LoaderCallbacks vous permettent de savoir quand créer un chargeur et d'indiquer à l'application qu'il est temps de cesser d'utiliser les données d'un chargeur.

LoaderManager.LoaderCallbacks inclut les méthodes suivantes:

  • onLoadFinished() : appelé lorsqu'un chargeur créé précédemment a terminé son chargement.
  • onLoaderReset() : appelé lorsqu'un chargeur créé précédemment est en cours de réinitialisation, ce qui rend ses données indisponibles.

Ces méthodes sont décrites plus en détail dans les sections suivantes.

onCreateLoader

Lorsque vous tentez d'accéder à un chargeur, par exemple via initLoader(), le système vérifie si le chargeur spécifié par l'ID existe. Si ce n'est pas le cas, il déclenche la méthode LoaderManager.LoaderCallbacks onCreateLoader(). C'est ici que vous créez un chargeur. Il s'agit généralement d'un CursorLoader, mais vous pouvez implémenter votre propre sous-classe Loader.

Dans l'exemple suivant, la méthode de rappel onCreateLoader() crée un CursorLoader à l'aide de sa méthode constructeur, qui nécessite l'ensemble complet des informations nécessaires pour envoyer une requête à ContentProvider. Plus précisément, il a besoin des éléments suivants:

  • uri: URI du contenu à récupérer.
  • projection: liste des colonnes à renvoyer. La transmission de null renvoie toutes les colonnes, ce qui est inefficace.
  • selection: filtre déclarant les lignes à renvoyer, mis en forme en tant que clause SQL WHERE (à l'exception de la clause WHERE elle-même). La transmission de null renvoie toutes les lignes correspondant à l'URI donné.
  • selectionArgs: si vous incluez des "?s" dans la sélection, ils sont remplacés par les valeurs de selectionArgs, dans l'ordre dans lequel ils apparaissent dans la sélection. Les valeurs sont liées sous forme de chaînes.
  • sortOrder: comment classer les lignes sous la forme d'une clause SQL ORDER BY (à l'exception de la clause ORDER BY elle-même). La transmission de null utilise l'ordre de tri par défaut, qui peut être non ordonné.

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

Cette méthode est appelée lorsqu'un chargeur créé précédemment termine son chargement. L'appel de cette méthode est garanti avant la publication des dernières données fournies pour ce chargeur. À ce stade, supprimez toute utilisation des anciennes données, puisqu’elles vont être libérées. Cependant, ne libérez pas les données vous-même : le chargeur en est le propriétaire et s'en charge.

Le chargeur libère les données une fois qu'il sait que l'application ne les utilise plus. Par exemple, si les données sont un curseur d'un CursorLoader, n'appelez pas close() sur celui-ci vous-même. Si le curseur est placé dans un CursorAdapter, utilisez la méthode swapCursor() pour que l'ancien Cursor ne soit pas fermé, comme illustré dans l'exemple suivant:

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

Cette méthode est appelée lorsqu'un chargeur créé précédemment est en cours de réinitialisation, ce qui rend ses données indisponibles. Ce rappel vous permet de savoir quand les données sont sur le point d'être publiées afin que vous puissiez supprimer votre référence à celles-ci.

Cette implémentation appelle swapCursor() avec la valeur 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);
}

Exemple

À titre d'exemple, voici l'implémentation complète d'un Fragment qui affiche un ListView contenant les résultats d'une requête envoyée au fournisseur de contenu de contacts. Il utilise un CursorLoader pour gérer la requête sur le fournisseur.

Étant donné que cet exemple provient d'une application permettant d'accéder aux contacts d'un utilisateur, son fichier manifeste doit inclure l'autorisation 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);
    }
}

Autres exemples

Les exemples suivants illustrent l'utilisation des chargeurs: