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 combinaison d'objets ViewModel et LiveData. Les modèles de vue survivent aux changements de configuration, comme les chargeurs, mais avec moins de code récurrent. LiveData permet de charger des données qui tiennent compte du cycle de vie et que vous pouvez réutiliser plusieurs modèles de vues. Vous pouvez également combiner LiveData en utilisant MediatorLiveData Toutes les requêtes observables, telles que celles provenant d'une La base de données Room peut être utilisée pour observer les modifications. aux données.

ViewModel et LiveData sont également disponibles si vous n'y avez pas accès à LoaderManager, comme dans un Service Utiliser les deux dans Le tandem permet d'accéder facilement aux données dont votre application a besoin sans avoir à gérer l'interface utilisateur. tout au long du cycle de vie. Pour en savoir plus sur LiveData, consultez les 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 une autre source de données à afficher dans une FragmentActivity ou Fragment.

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

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

Les chargeurs permettent de résoudre ces problèmes et présentent 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 persistent et mettent en cache les résultats en cas de modification de la configuration pour empêcher requêtes en double.
  • Les chargeurs peuvent implémenter un observateur pour surveiller les modifications apportées aux source de données. Par exemple, CursorLoader automatiquement enregistre 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 lors de l'utilisation chargeurs dans une application. Elles sont résumées dans le tableau suivant:

Classe/Interface Description
LoaderManager Une classe abstraite associée à un élément FragmentActivity ou Fragment pour gérer un ou plusieurs Loader instances. Il n'y en a qu'une LoaderManager par activité ou fragment, mais une LoaderManager peut gérer plusieurs chargeurs.

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

Pour commencer à charger des données à partir d'un chargeur, appelez soit initLoader() ou restartLoader() Le système détermine automatiquement si un chargeur avec le même identifiant entier et crée un nouveau chargeur ou réutilise un chargeur existant.

LoaderManager.LoaderCallbacks Cette interface contient des méthodes de rappel qui sont appelées lorsque se produisent. L'interface définit trois méthodes de rappel: <ph type="x-smartling-placeholder">
    </ph>
  • onCreateLoader(int, Bundle): appelé lorsque le système a besoin de créer un chargeur. Dans votre code, créez un objet Loader et renvoyez-le à le système.
  • onLoadFinished(Loader<D>, D): appelé lorsqu'un chargeur a fini de charger des données. Généralement, à présenter les données à l'utilisateur dans votre code.
  • onLoaderReset(Loader<D>): appelé lorsqu'un chargeur précédemment créé est en cours de réinitialisation, lorsque vous appelez destroyLoader(int) ou lorsque l'activité ou fragment est détruit, ce qui rend ses données indisponibles. Dans votre code, supprimer toute référence aux données du chargeur.
Votre activité ou fragment implémente généralement cette interface, qui est enregistré lorsque vous appelez initLoader() ou restartLoader()
Loader Les chargeurs effectuent le chargement des données. Cette classe est abstraite comme classe de base pour tous les chargeurs. Vous pouvez directement sous-classer Loader ou utiliser l'un des outils intégrés suivants pour simplifier l'implémentation: <ph type="x-smartling-placeholder">

Les sections suivantes vous expliquent comment utiliser ces de classe et d'interface d'une application.

Utiliser des chargeurs dans une application

Cette section explique comment utiliser des chargeurs dans une application Android. Une 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 FragmentActivity ou Fragment Il n'y a qu'un seul élément LoaderManager par activité ou fragment.

Habituellement, initialisez un Loader dans la méthode onCreate() de l'activité ou dans la méthode onCreate(). Toi 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() prend les paramètres suivants:

  • Identifiant unique qui identifie le chargeur. Dans cet exemple, l'ID est 0.
  • Arguments facultatifs à fournir au chargeur de construction (null dans cet exemple).
  • Une implémentation LoaderManager.LoaderCallbacks, qui les appels LoaderManager pour signaler les événements de chargeur. Dans ce exemple, la classe locale implémente l'interface LoaderManager.LoaderCallbacks, qui transmet donc une référence à elle-même, this.

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

  • Si le chargeur spécifié par l'ID existe déjà, il s'agit du dernier chargeur créé. est réutilisé.
  • Si le chargeur spécifié par l'ID n'existe pas, initLoader() déclenche 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 concernant onCreateLoader.

Dans les deux cas, la valeur LoaderManager.LoaderCallbacks donnée est associée au chargeur et est appelée lorsque le les changements d'état du chargeur. Si, au moment de cet appel, l'appelant est dans son et que le chargeur demandé existe déjà et a généré son 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 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 maintient l'état du chargeur. et les contenus associés.

Comme cela l'implique, vous interagissez rarement avec les chargeurs directement. Vous utilisez le plus souvent les méthodes LoaderManager.LoaderCallbacks pour intervenir dans le 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(), en tant que comme indiqué dans la section précédente, elle utilise un chargeur existant avec l'ID spécifié, le cas échéant. S'il n'y en a pas, il en crée un. Mais vous pouvez parfois vouloir supprimer vos anciennes données et recommencez.

Pour supprimer vos anciennes données, utilisez restartLoader(). Par exemple : implémentation de SearchView.OnQueryTextListener redémarrages le chargeur lorsque la requête de l'utilisateur change. Le chargeur doit être redémarré pour que qu'il puisse utiliser le nouveau filtre de recherche 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 de conserver leurs données après leur arrêt. Cela permet aux applications de conserver des données dans les méthodes onStop() et onStart() de l'activité ou du fragment, de sorte que Lorsque les utilisateurs retournent dans une application, ils n'ont pas à attendre que les données actualiser la page.

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

LoaderManager.LoaderCallbacks inclut ces éléments méthodes:

  • onLoadFinished(): appelé lorsqu'un chargeur précédemment créé a terminé son chargement.
  • onLoaderReset(): appelé lorsqu'un chargeur précédemment créé est réinitialisé, ce qui rend son 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(), il vérifie si le chargeur spécifié par l'ID existe. Si ce n'est pas le cas, elle déclenche la méthode LoaderManager.LoaderCallbacks onCreateLoader(). Ce est l'endroit où vous créez un chargeur. Il s'agit généralement d'une CursorLoader, mais vous pouvez implémenter votre propre sous-classe Loader.

Dans l'exemple suivant, onCreateLoader() de rappel 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 au ContentProvider. Plus précisément, il a besoin des éléments suivants:

  • uri: URI du contenu à récupérer.
  • projection: une liste des colonnes à renvoyer. Réussite null renvoie toutes les colonnes, ce qui est inefficace.
  • selection: un filtre déclarant les lignes à renvoyer. formaté en tant que clause SQL WHERE (à l'exclusion de WHERE lui-même). Réussite null renvoie toutes les lignes pour l'URI donné.
  • selectionArgs: si vous incluez "?" dans la sélection, sont remplacées par les valeurs de selectionArgs dans l'ordre dans lequel elles apparaissent de la sélection. Les valeurs sont liées sous forme de chaînes.
  • sortOrder: comment organiser les lignes au format SQL la clause ORDER BY (à l'exclusion de la clause ORDER BY elle-même). Réussite : 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 précédemment créé termine son chargement. L'appel de cette méthode est garanti avant la publication des dernières données fourni pour ce chargeur. À ce stade, supprimez toute utilisation les anciennes données, puisqu'elles vont être publiées. Mais ne publiez pas les données vous-même : le chargeur en est le propriétaire et s'en occupe.

Le chargeur libère les données lorsqu'il sait que l'application n'est plus l'utilisent. Par exemple, si les données sont un curseur d'un CursorLoader, n'appelez pas close() vous-même. Si le curseur se trouve placé dans un CursorAdapter, utilisez la méthode swapCursor() pour que l'ancienne Cursor n'est pas fermée, 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);
}

Réinitialiser onLoader

Cette méthode est appelée lorsqu'un chargeur précédemment créé 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 libérée afin que vous puissiez supprimer votre référence à celui-ci.

Cette implémentation fait appel 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 sur le fournisseur de contenu des contacts. Il utilise un CursorLoader pour gérer la requête sur le fournisseur.

Comme cet exemple provient d'une application permettant d'accéder aux contacts d'un utilisateur, son le 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:

  • <ph type="x-smartling-placeholder"></ph> 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 du fournisseur de contacts.
  • <ph type="x-smartling-placeholder"></ph> LoaderThrottle: exemple d'utilisation de la limitation pour réduire le nombre de requêtes effectuées par un fournisseur de contenu lorsque ses données changent.