Présentation de la bibliothèque Paging 2 Inclus dans Android Jetpack.

La bibliothèque Paging vous permet de charger et d'afficher de petits morceaux de données en même temps. Le chargement de données partielles à la demande réduit l'utilisation de la bande passante du réseau et des ressources système.

Ce guide fournit plusieurs exemples conceptuels de la bibliothèque, ainsi qu'un aperçu de son fonctionnement. Pour voir des exemples complets illustrant le fonctionnement de cette bibliothèque, accédez à l'atelier de programmation et consultez des exemples dans la section Autres ressources.

Configuration

Pour importer des composants Paging dans votre application Android, ajoutez les dépendances suivantes au fichier build.gradle de votre application :

Groovy

dependencies {
  def paging_version = "2.1.2"

  implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}

Kotlin

dependencies {
  val paging_version = "2.1.2"

  implementation("androidx.paging:paging-runtime:$paging_version") // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation("androidx.paging:paging-common:$paging_version") // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation("androidx.paging:paging-rxjava2:$paging_version") // For Kotlin use paging-rxjava2-ktx
}

Architecture de la bibliothèque

Cette section décrit et présente les principaux composants de la bibliothèque Paging.

PagedList

Le composant clé de la bibliothèque Paging correspond à la classe PagedList, qui charge des fragments de données, ou des pages de votre application. À mesure que des données supplémentaires sont nécessaires, elles sont paginées dans l'objet PagedList existant. En cas de modification des données chargées, une nouvelle instance de PagedList est émise vers le conteneur de données observables depuis un objet basé sur LiveData ou RxJava2. Lorsque des objets PagedList sont générés, l'interface utilisateur de votre application présente son contenu, tout en respectant les cycles de vie de vos contrôleurs d'interface utilisateur.

L'extrait de code suivant montre comment configurer le modèle de visualisation de votre application pour charger et présenter des données à l'aide d'un conteneur LiveData d'objets PagedList :

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

Java

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    // Creates a PagedList object with 50 items per page.
    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
                concertDao.concertsByDate(), 50).build();
    }
}

Données

Chaque instance de PagedList charge un instantané à jour des données de votre application à partir de l'objet DataSource correspondant. Les données sont acheminées du backend ou de la base de données de votre application vers l'objet PagedList.

L'exemple suivant utilise la Bibliothèque de persistance Room pour organiser les données de votre application, mais si vous souhaitez les stocker par un autre moyen, vous pouvez également fournir votre propre fabrique de source de données.

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

Java

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a
    // PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

Pour savoir comment charger des données dans des objets PagedList, consultez le guide sur le chargement des données paginées.

Interface utilisateur

La classe PagedList fonctionne avec un objet PagedListAdapter pour charger des éléments dans un objet RecyclerView. Ces classes fonctionnent en synergie pour récupérer et afficher le contenu lors de son chargement, en préchargeant le contenu hors écran et en animant les modifications de contenu.

Pour en savoir plus, consultez le guide sur l'affichage des listes paginées.

Assurer la compatibilité avec différentes architectures de données

La bibliothèque Paging est compatible avec les architectures de données suivantes :

  • Diffusées uniquement à partir d'un serveur backend.
  • Stockées uniquement dans une base de données sur l'appareil.
  • Association des autres sources utilisant la base de données sur l'appareil en tant que cache.

La figure 1 montre comment les données circulent dans chacune de ces configurations d'architecture. Dans le cas d'une solution exclusivement basée sur un réseau ou une base de données, les données sont transmises directement au modèle d'interface utilisateur de votre application. Si vous utilisez une approche combinée, les données sont transmises depuis votre serveur backend vers une base de données sur l'appareil, puis vers le modèle d'interface utilisateur de votre application. De temps en temps, le point de terminaison de chaque flux de données manque de données à charger, et demande des données supplémentaires au composant qui a fourni les données. Par exemple, lorsqu'une base de données sur appareil manque de données, elle demande plus de données au serveur.

Diagramme des flux de données
Figure 1 : Comment les données transitent-elles dans chacune des architectures compatibles avec la bibliothèque Paging ?

Le reste de cette section fournit des recommandations pour configurer chaque cas d'utilisation de flux de données.

Réseau uniquement

Pour afficher les données d'un serveur backend, utilisez la version synchrone de l'API Retrofit pour charger des informations dans votre propre objet DataSource personnalisé.

Base de données uniquement

Configurez votre RecyclerView pour observer le stockage local, de préférence à l'aide de la bibliothèque de persistance Room. Ainsi, chaque fois que des données sont insérées ou modifiées dans la base de données de votre application, ces modifications sont automatiquement répercutées dans la RecyclerView qui affiche ces données.

Réseau et base de données

Après avoir commencé à observer la base de données, vous pouvez utiliser PagedList.BoundaryCallback pour savoir quand la base de données est à court de données. Vous pouvez ensuite récupérer d'autres éléments de votre réseau et les insérer dans la base de données. Si votre interface utilisateur observe la base de données, vous n'avez rien d'autre à faire.

Gérer les erreurs réseau

Lorsque vous utilisez un réseau pour récupérer ou paginer les données que vous affichez à l'aide de la bibliothèque Paging, il est important de ne pas considérer que le réseau est soit "disponible", soit "indisponible" en permanence, car de nombreuses connexions sont intermittentes ou erratiques :

  • Un serveur peut ne pas répondre à une requête réseau.
  • L'appareil est peut-être connecté à un réseau lent ou faible.

Au lieu de cela, votre application doit vérifier les échecs de chaque requête et récupérer aussi efficacement que possible lorsque le réseau n'est pas disponible. Par exemple, vous pouvez ajouter un bouton "Réessayer" que les utilisateurs pourront sélectionner si l'étape d'actualisation des données ne fonctionne pas. Si une erreur se produit lors de l'étape de pagination des données, il est préférable de relancer automatiquement les requêtes de pagination.

Mettre à jour votre application existante

Si votre application consomme déjà des données à partir d'une base de données ou d'une source backend, il est possible de passer directement à une fonctionnalité fournie par la bibliothèque Paging. Cette section explique comment mettre à jour une application qui présente un design courant.

Solutions de pagination personnalisées

Si vous utilisez une fonctionnalité personnalisée pour charger de petits sous-ensembles de données à partir de la source de données de votre application, vous pouvez remplacer cette logique par celle de la classe PagedList. Les instances de PagedList offrent des connexions intégrées aux sources de données courantes. Ces instances fournissent également des adaptateurs pour les objets RecyclerView que vous pouvez inclure dans l'interface utilisateur de votre application.

Données chargées à l'aide de listes au lieu de pages

Si vous utilisez une liste en mémoire comme structure de sauvegarde de données pour l'adaptateur de votre interface utilisateur, envisagez d'observer des mises à jour de données à l'aide d'une classe PagedList si le nombre d'éléments peut s'afficher en grand. Les instances de PagedList peuvent utiliser LiveData<PagedList> ou Observable<List> pour transmettre les mises à jour de données à l'interface utilisateur de votre application, ce qui réduit les temps de chargement et l'utilisation de la mémoire. Mieux encore, le remplacement d'un objet List par un objet PagedList dans votre application ne nécessite aucune modification de la structure de l'interface utilisateur ni de la logique de mise à jour des données.

Associer un curseur de données à une vue de liste à l'aide de CursorAdapter

Votre application peut utiliser un CursorAdapter pour associer les données d'un Cursor à une ListView. Dans ce cas, vous devez généralement effectuer la migration depuis une ListView vers une RecyclerView comme conteneur de l'interface utilisateur de la liste des applications, puis remplacer le composant Cursor par Room ou PositionalDataSource, selon que les instances de Cursor accèdent à une base de données SQLite.

Dans certains cas, par exemple lorsque vous travaillez avec des instances de Spinner, vous ne fournissez que l'adaptateur lui-même. Une bibliothèque prend ensuite les données chargées dans cet adaptateur et les affiche pour vous. Dans ce cas, remplacez le type de données de votre adaptateur par LiveData<PagedList>, puis encapsulez cette liste dans un objet ArrayAdapter avant d'essayer de faire en sorte qu'une classe de bibliothèque gonfle ces éléments dans une interface utilisateur.

Charger du contenu de manière asynchrone à l'aide d'AsyncListUtil

Si vous utilisez des objets AsyncListUtil pour charger et afficher des groupes d'informations de manière asynchrone, la bibliothèque Paging vous permet de charger des données plus facilement :

  • Vos données n'ont pas besoin d'être positionnées. La bibliothèque Paging vous permet de charger des données directement à partir de votre backend à l'aide de clés fournies par le réseau.
  • Vos données peuvent être extrêmement volumineuses. Avec la bibliothèque Paging, vous pouvez charger des données dans des pages jusqu'à ce qu'il ne reste plus aucune donnée.
  • Vous pouvez observer vos données plus facilement. La bibliothèque Paging peut présenter les données que le ViewModel de votre application contient dans une structure de données observable.

Exemples de bases de données

Les extraits de code suivants montrent plusieurs façons de faire fonctionner tous les éléments ensemble.

Observer les données paginées avec LiveData

L'extrait de code suivant montre tous les composants qui fonctionnent ensemble. À mesure que des événements de concert sont ajoutés, supprimés ou modifiés dans la base de données, le contenu de la RecyclerView est mis à jour automatiquement et efficacement :

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

class ConcertActivity : AppCompatActivity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: ConcertViewModel by viewModels()
        val recyclerView = findViewById(R.id.concert_list)
        val adapter = ConcertAdapter()
        viewModel.concertList.observe(this, PagedList(adapter::submitList))
        recyclerView.setAdapter(adapter)
    }
}

class ConcertAdapter() :
        PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
    fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
        val concert: Concert? = getItem(position)

        // Note that "concert" is a placeholder if it's null.
        holder.bindTo(concert)
    }

    companion object {
        private val DIFF_CALLBACK = object :
                DiffUtil.ItemCallback<Concert>() {
            // Concert details may have changed if reloaded from the database,
            // but ID is fixed.
            override fun areItemsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert.id == newConcert.id

            override fun areContentsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert == newConcert
        }
    }
}

Java

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
            concertDao.concertsByDate(), /* page size */ 50).build();
    }
}

public class ConcertActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ConcertViewModel viewModel =
                new ViewModelProvider(this).get(ConcertViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.concert_list);
        ConcertAdapter adapter = new ConcertAdapter();
        viewModel.concertList.observe(this, adapter::submitList);
        recyclerView.setAdapter(adapter);
    }
}

public class ConcertAdapter
        extends PagedListAdapter<Concert, ConcertViewHolder> {
    protected ConcertAdapter() {
        super(DIFF_CALLBACK);
    }

    @Override
    public void onBindViewHolder(@NonNull ConcertViewHolder holder,
            int position) {
        Concert concert = getItem(position);
        if (concert != null) {
            holder.bindTo(concert);
        } else {
            // Null defines a placeholder item - PagedListAdapter automatically
            // invalidates this row when the actual object is loaded from the
            // database.
            holder.clear();
        }
    }

    private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<Concert>() {
        // Concert details may have changed if reloaded from the database,
        // but ID is fixed.
        @Override
        public boolean areItemsTheSame(Concert oldConcert, Concert newConcert) {
            return oldConcert.getId() == newConcert.getId();
        }

        @Override
        public boolean areContentsTheSame(Concert oldConcert,
                Concert newConcert) {
            return oldConcert.equals(newConcert);
        }
    };
}

Observer les données paginées à l'aide de RxJava2

Si vous préférez utiliser RxJava2 au lieu de LiveData, vous pouvez créer un objet Observable ou Flowable :

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: Observable<PagedList<Concert>> =
            concertDao.concertsByDate().toObservable(pageSize = 50)
}

Java

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final Observable<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;

        concertList = new RxPagedListBuilder<>(
                concertDao.concertsByDate(), /* page size */ 50)
                        .buildObservable();
    }
}

Vous pouvez ensuite démarrer et arrêter l'observation des données à l'aide du code de l'extrait suivant :

Kotlin

class ConcertActivity : AppCompatActivity() {
    private val adapter = ConcertAdapter()

    // Use the 'by viewModels()' Kotlin property delegate
    // from the activity-ktx artifact
    private val viewModel: ConcertViewModel by viewModels()

    private val disposable = CompositeDisposable()

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val recyclerView = findViewById(R.id.concert_list)
        recyclerView.setAdapter(adapter)
    }

    override fun onStart() {
        super.onStart()
        disposable.add(viewModel.concertList
                .subscribe(adapter::submitList)))
    }

    override fun onStop() {
        super.onStop()
        disposable.clear()
    }
}

Java

public class ConcertActivity extends AppCompatActivity {
    private ConcertAdapter adapter = new ConcertAdapter();
    private ConcertViewModel viewModel;

    private CompositeDisposable disposable = new CompositeDisposable();

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        RecyclerView recyclerView = findViewById(R.id.concert_list);

        viewModel = new ViewModelProvider(this).get(ConcertViewModel.class);
        recyclerView.setAdapter(adapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        disposable.add(viewModel.concertList
                .subscribe(adapter.submitList(flowableList)
        ));
    }

    @Override
    protected void onStop() {
        super.onStop();
        disposable.clear();
    }
}

Le code pour ConcertDao et ConcertAdapter est le même pour une solution basée sur RxJava2 et pour une solution basée sur LiveData.

Envoyer des commentaires

Faites-nous part de vos commentaires et de vos idées via les ressources suivantes :

Issue Tracker
Signalez les problèmes pour que nous puissions corriger les bugs.

Ressources supplémentaires

Pour en savoir plus sur la bibliothèque Paging, consultez les ressources suivantes.

Exemples

Ateliers de programmation

Vidéos