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 queonDestroy()
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 unContentObserver
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 Pour commencer à charger des données à partir d'un chargeur, appelez |
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 :
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:
FragmentActivity
ouFragment
.- Instance de
LoaderManager
. - Un
CursorLoader
pour charger des données sauvegardées par unContentProvider
. Vous pouvez également implémenter votre propre sous-classe deLoader
ouAsyncTaskLoader
pour charger des données à partir d'une autre source. - Implémentation pour
LoaderManager.LoaderCallbacks
. C'est ici que vous créez des chargeurs et que vous gérez vos références aux chargeurs existants. - Moyen d'afficher les données du chargeur, par exemple
SimpleCursorAdapter
. - Une source de données, telle qu'une
ContentProvider
, lorsque vous utilisez unCursorLoader
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
, queLoaderManager
appelle pour signaler les événements du chargeur. Dans cet exemple, la classe locale implémente l'interfaceLoaderManager.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éthodeLoaderManager.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 suronCreateLoader
.
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:
onCreateLoader()
: instancie et renvoie un nouveauLoader
pour l'ID donné.
-
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:
- LoaderCursor: version complète de l'extrait précédent.
- Récupérer une liste de contacts : tutoriel qui utilise un
CursorLoader
pour récupérer des données auprès du fournisseur de contacts. - LoaderThrottle: exemple d'utilisation de la limitation pour réduire le nombre de requêtes qu'un fournisseur de contenu effectue lorsque ses données changent.