Coroutines avec bibliothèque de persistance de pièce

Online Coding Courses for Kids

Pièce est la bibliothèque de composants d’architecture de Google pour travailler avec SQLite sur Android. Avec la sortie de la version 2.1, la bibliothèque a ajouté la prise en charge des transactions de base de données à l’aide de coroutines.

Dans ce didacticiel, vous allez apprendre à:

  • Mettre en œuvre des fonctions de suspension sur Objets d’accès aux données (ou DAO) dans la chambre.
  • Appelez-les à l’aide des coroutines de Kotlin pour les transactions de base de données.

Au fur et à mesure de votre progression, vous prendrez une application répertoriant les 20 meilleurs joueurs de tennis et la refactoriserez pour utiliser les fonctions de suspension et les coroutines. Vous ajouterez également quelques nouvelles fonctionnalités, notamment l’affichage des détails du joueur, la sélection des joueurs favoris et la suppression des joueurs.

Ce didacticiel suppose une compréhension de base de la façon de créer des applications Android, de travailler avec le Bibliothèque de persistance de pièce et utiliser le modèle de threading du framework Android avec le langage de programmation Kotlin. Une expérience avec le DSL Kotlin Gradle est utile mais pas obligatoire.

Si vous n’avez pas d’expérience avec Room, veuillez consulter l’article Persistance des données avec Room pour une introduction. Si vous n’avez pas d’expérience avec les coroutines, lisez d’abord le tutoriel Kotlin Coroutines pour Android: mise en route. Revenez ensuite à ce didacticiel. Sinon, procédez à vos risques et périls. :]

Commencer

Pour commencer, téléchargez les ressources du projet depuis le Télécharger les documents en haut ou en bas de ce didacticiel.

Importez le TennisPlayers-starter projet en Android Studio et laisse Gradle synchroniser le projet.

Créez et exécutez l’application.

Si tout se compile, vous verrez une liste des meilleurs joueurs de tennis au monde.

liste des meilleurs joueurs de tennis

Bon travail! Vous êtes opérationnel.

La liste est chargée à partir d’une base de données Room implémentée dans PlayersDatabase.kt fichier.
Regardez bien, et vous verrez un problème avec la mise en œuvre. À l’intérieur de getDatabase() fonction, sous la synchronized bloc, vous remarquerez RoomDatabase.Builder a un appel à allowMainThreadQueries() , ce qui signifie que toutes les opérations de base de données s’exécuteront sur le fil principal.

Exécution de transactions de base de données sur le MainThread est en fait mauvais, car cela entraînerait un gel de l’interface utilisateur et / ou un crash d’application.

Il est temps de résoudre ce problème avec la puissance des coroutines.

Préremplissage de la base de données

Localiser players.json dans res / raw à l’intérieur app module. Cependant, analyser ce fichier et le placer dans la base de données peut être une opération coûteuse. Ce n’est certainement pas quelque chose qui devrait être sur le fil principal.

Idéalement, vous souhaitez insérer les données pendant la création de la base de données. Room fournit ce mécanisme sous la forme de RoomDatabase.Callback. Ce rappel vous permet d’intercepter la base de données lors de son ouverture ou de sa création. Il vous permet également de connecter votre propre code au processus. Vous configurerez ensuite le rappel.

Création de la RoomDatabase.Callback

Remplacer // TODO: Add PlayerDatabaseCallback here dans PlayersDatabase.kt avec le code fourni ci-dessous:

private class PlayerDatabaseCallback(
      private val scope: CoroutineScope,
      private val resources: Resources
  ) : RoomDatabase.Callback() {

  override fun onCreate(db: SupportSQLiteDatabase) {
    super.onCreate(db)
    INSTANCE?.let { database ->
      // TODO: dispatch some background process to load our data from Resources
    }
  }
  // TODO: Add prePopulateDatabase() here
}

Ici, vous définissez une classe concrète de RoomDatabase.Callback. Notez que le constructeur de classe accepte Ressources comme argument. Ceci est nécessaire pour charger le fichier JSON depuis res / raw. L’autre argument passé est le CoroutineScope, qui est utilisé pour distribuer le travail en arrière-plan. Ceci sera discuté plus en détail dans la section suivante.

getDatabase() dans le Objet compagnon doit éventuellement définir une instance de votre rappel dans le générateur. Pour ce faire, vous devrez modifier la signature pour passer CoroutineScope et Ressources comme arguments.

Mise à jour getDatabase(context: Context) avec la signature suivante:

fun getDatabase(
    context: Context,
    coroutineScope: CoroutineScope, // 1
    resources: Resources // 2
): PlayersDatabase { /* ...ommitted for brevity */}

Ensuite, remplacez allowMainThreadQueries() à l’intérieur Room.databaseBuilder avec le addCallback comme indiqué ci-dessous:

val instance = Room.databaseBuilder(context.applicationContext,
            PlayersDatabase::class.java,
            "players_database")
            .addCallback(PlayerDatabaseCallback(coroutineScope, resources)) 
            .build()

Le rappel est entièrement connecté. Il est temps de lancer une coroutine à partir de votre rappel pour faire un peu de travail.

Explorer CoroutineScope

CoroutineScope définit une nouvelle étendue pour les coroutines. Cela signifie que les éléments de contexte et les annulations sont propagés automatiquement aux coroutines enfants s’exécutant à l’intérieur. Différents types de portées peuvent être utilisés lors de l’examen de la conception de votre application. Les étendues se lient généralement en interne à un Job pour assurer une concurrence structurée.

Étant donné que les fonctions du générateur de coroutine sont des extensions sur CoroutineScope, démarrer une coroutine est aussi simple que d’appeler launch et async parmi d’autres méthodes de génération directement à l’intérieur de la classe Coroutine-Scoped.

Quelques types de portée:

  • GlobalScope: Une étendue liée à l’application. Utilisez-le lorsque le composant en cours d’exécution n’est pas facilement détruit. Par exemple, dans Android, utiliser cette étendue de la classe d’application devrait être OK. Il n’est cependant pas recommandé de l’utiliser à partir d’une activité. Imaginez que vous lanciez une coroutine à partir de la portée mondiale. L’activité est détruite, mais la demande n’est pas terminée au préalable. Cela peut provoquer un crash ou une fuite de mémoire dans votre application.
  • Portée de ViewModel: Une étendue liée à un modèle de vue. Utilisez-le lorsque vous incluez les composants de l’architecture bibliothèque ktx. Cette portée lie les coroutines au modèle de vue. Lorsqu’il est détruit, les coroutines s’exécutant dans le contexte de ViewModel seront automatiquement annulées.
  • Portée personnalisée: Une portée liée à un objet étendant la portée de Coroutine. Lorsque vous étendez CoroutineScope à partir de votre objet et le liez à un élément associé Job, vous pouvez gérer les coroutines exécutées dans cette étendue. Par exemple, vous appelez job = Job() de votre activité onCreate et job.cancel() de onDestroy() pour annuler toute coroutine exécutée dans la portée personnalisée de ce composant.

Ensuite, vous utiliserez ces connaissances sur CoroutineScope lorsque vous commencerez à charger les données du lecteur en arrière-plan à l’aide de coroutines pour les garder sous contrôle.

Chargement des joueurs en arrière-plan

Avant de vous inquiéter le travail se déroulera, vous devez d’abord définir le travail à effectuer.

Pour ce faire, accédez à PlayersDatabase.kt fichier. Juste en dessous du onCreate() remplacer à l’intérieur PlayerDatabaseCallback, remplacer // TODO: Add prePopulateDatabase() here avec le code ci-dessous::

private fun prePopulateDatabase(playerDao: PlayerDao){
  // 1
  val jsonString = resources.openRawResource(R.raw.players).bufferedReader().use {
    it.readText()
  }
  // 2
  val typeToken = object : TypeToken>() {}.type
  val tennisPlayers = Gson().fromJson>(jsonString, typeToken)
  // 3
  playerDao.insertAllPlayers(tennisPlayers)
}

Vous voilà:

  1. Lire le players.json fichier de ressources brutes dans un String.
  2. Le convertir en un List en utilisant Gson.
  3. L’insérer dans la base de données Room à l’aide de la playerDao.

CoroutineScopes fournit plusieurs constructeurs de coroutine pour démarrer le travail en arrière-plan. Lorsque vous souhaitez simplement tirer et oublier certains travaux d’arrière-plan, sans vous soucier d’une valeur de retour, le choix approprié consiste à utiliser launch constructeur de coroutine.

Copiez le code suivant en remplaçant // TODO: dispatch some background process to load our data from Resources dans onCreate() de PlayerDatabaseCallback:

//1
scope.launch{
   val playerDao = database.playerDao() // 2
   prePopulateDatabase(playerDao) // 3
}

Vous voilà:

  1. Appeler le launch constructeur de coroutine sur le CoroutineScope passé à PlayerDatabaseCallback nommé en tant que scope
  2. Accéder à playerDao.
  3. Appeler le prePopulateDatabase(playerDoa) fonction que vous avez définie précédemment.

Bon travail! Créez et exécutez l’application maintenant. Cela a-t-il fonctionné?

Vous remarquerez que l’application ne s’exécute plus car vous avez mis à jour getDatabase() Signature. Il est temps de résoudre ce problème.

Fournir CoroutineScope

Ouvrez le PlayerViewModel.kt et DetailViewModel.kt des dossiers. Mettre à jour le getDatabase() fonction dans playerDao comme indiqué ci-dessous:

val playerDao = PlayersDatabase
  .getDatabase(application, viewModelScope, application.resources)
  .playerDao()

Ici, vous passez dans le viewModelScope CoroutineScope du bibliothèque lifecycle-viewmodel-ktx pour permettre à la base de données d’utiliser cette étendue lors de l’exécution de coroutines. En utilisant viewModelScope, toute coroutine en cours d’exécution sera annulée lorsque AndroidViewModel est détruit. Les ressources de l’application sont également transmises au getDatabase() appeler comme requis par la nouvelle signature de fonction.

À ce stade, vous pouvez créer et exécuter l’application, mais vous verrez un IllegalStateException jeté avec le message suivant:

Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

Ceci arrive pour deux raisons:

  1. PlayerDao méthodes pour getPlayerCount() et insertAllPlayers(players: List) accèdent toujours à la base de données sur le thread principal.
  2. L’ancien code dans le MainActivity.kt le fichier exécute des requêtes sur le thread principal.

Mais attendez! CoroutineScope launch le générateur de coroutine pousse ce travail vers une coroutine, mais Room ne le sait pas encore. La vérification interne à l’intérieur de la bibliothèque de pièce échoue même si vous appuyez sur la touche MainActivity travailler à une coroutine. C’est parce que les méthodes DAO manquent quelque chose de très important: suspend mot-clé.

Suspension des fonctions

Dans Kotlin, une fonction de suspension est une fonction qui peut suspendre l’exécution d’une coroutine. Cela signifie que la coroutine peut suspendre, reprendre ou annuler. Cela signifie également que la fonction peut exécuter un certain comportement de longue durée et attendre sa fin avec d’autres appels de fonction de suspension.

L’application doit vérifier le nombre de joueurs dans la base de données avant de remplir, vous devez donc appeler les méthodes l’une après l’autre avec suspend mot-clé. Pour tirer parti de ce comportement avec Room, vous mettrez à jour PlayerDao en ajoutant le suspend mot-clé à ses définitions de méthode.

Tout d’abord, ouvrez le PlayerDao.kt déposer et ajouter suspend mot-clé à insertAllPlayers(players: List).

Copiez le code suivant et collez-le à la place de la définition existante:

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllPlayers(players: List)

Ici, vous avez ajouté suspend pour indiquer à Room que cette méthode peut suspendre son exécution et exécuter ses opérations à l’aide d’une coroutine.

Ensuite, ouvrez le PlayersDatabase.kt déposer et ajouter le suspend mot-clé à prePopulateDatabase(playerDao):

private suspend fun prePopulateDatabase(playerDao: PlayerDao) {
  //... omitted for brevity
}

Vous avez mis à jour prePopulateDatabase(playerDao: PlayerDao) à exécuter en tant que fonction de suspension à partir du générateur de coroutine de lancement.

Cette modification modifiera l’appel pour insérer toutes les méthodes à exécuter dans une coroutine. Lors de la création de la base de données, le rappel appellera le prePopulateDatabase (playerDao: PlayerDao) suspendre la fonction et insérer tous les lecteurs lus à partir du fichier JSON brut.

Ensuite, ouvrez le PlayerRepository.kt fichier. Mettez en surbrillance et supprimez fun insertAllPlayers(players: List) une fonction. Vous n’en aurez plus besoin.

Depuis insertAllPlayers() est supprimée, donc tout endroit dans le code où elle est référencée ne sera plus résolu. Cela entraînera des erreurs de compilation. Vous devrez vous en débarrasser.
Ouvrez le PlayerViewModel.kt fichier. Mettez en surbrillance et supprimez populateDatabase().

Ensuite, ouvrez le MainActivity.kt fichier. Mettez en surbrillance et supprimez playerViewModel.populateDatabase() depuis le populateDatabase() a été supprimé du playerViewModel.

À ce stade, vous avez presque terminé les mises à jour. cependant, MainActivity interroge toujours la base de données sur le thread principal. Pour résoudre ce problème, vous devrez observer les modifications dans la base de données au lieu de les interroger. Il est temps de mettre à jour le PlayerDao.kt, PlayerRepository.kt, PlayerViewModel.kt et MainActivity.kt fichiers à utiliser Données en direct.

Observer les modifications des données

En ce moment, vous exécutez la fonctionnalité de pré-remplissage lorsque MainActivity instancie PlayerViewModel. En tant que tel, vous ne pouvez pas interroger la base de données immédiatement, car Room n’autorisera pas plusieurs connexions à la base de données simultanément.

Pour contourner cette restriction, vous devez ajouter LiveData autour du type de retour de getAllPlayers() et observer les changements plutôt que de rechercher le List.

Tout d’abord, ouvrez le PlayerDao.kt déposer et modifier getAllPlayers() pour avoir le code suivant:

@Query("SELECT id, firstName, lastName, country, favorite, imageUrl FROM players")
fun getAllPlayers(): LiveData>

Ici, tu t’es enveloppé List dans un LiveData objet.

Remarque: Puisque vous renvoyez un objet LiveData, il n’est pas nécessaire d’utiliser suspend sur cette méthode. En fait, Room ne le permettra même pas. L’objet LiveData repose sur le modèle d’observateur dans lequel l’appelant peut s’abonner aux modifications de la valeur qu’il contient. Chaque fois que de nouvelles données sont disponibles dans la base de données, cette liste est mise à jour et reflète ces données dans l’interface utilisateur. Il n’aura pas besoin de réinterroger la base de données.

Ensuite, ouvrez le PlayerRespository.kt déposer et mettre à jour le getAllPlayers() signature de la méthode:

  fun getAllPlayers(): LiveData> {
    return playerDao.getAllPlayers()
  }

Ensuite, ouvrez le PlayerViewModel.kt fichier et faites de même:

  fun getAllPlayers(): LiveData> {
    return repository.getAllPlayers()
  }

Enfin, vous devez corriger la liste. Ouvrez le MainActivity.kt fichier. Supprimer tout le code ci-dessous //TODO Replace below lines with viewmodel observation ainsi que le Todo lui-même, puis attachez Observer à la playerViewModel.getAllPlayers() comme indiqué ci-dessous:

playerViewModel.getAllPlayers().observe(this, Observer> { players ->
  adapter.swapData(players)
})

Créez et exécutez l’application. La liste se rétablit!

liste restaurée avec coroutines

Hou la la! Vous avez fait beaucoup de travail pour mettre ces changements en œuvre. Maintenant que tout est terminé, vous pouvez améliorer l’application en ajoutant des fonctionnalités favorites et supprimées à l’écran des détails du lecteur.

Avant d’essayer cette mise à jour, allez-y et appuyez sur n’importe quel joueur de la liste. Vous remarquerez que les détails du joueur sont manquants.

détails du joueur cassés

Il est temps de remettre les pendules à l’heure. :]

Obtenir un seul joueur

Pour récupérer un Player à partir de la base de données, certaines choses doivent se produire. Première, DetailFragment doit accéder à la PlayerListItem à partir des arguments de fragment – cela a déjà été mis en œuvre pour vous.

Puis PlayerListItem«S id doit passer à une nouvelle méthode getPlayer(id: Int): Player de votre DAO. N’oubliez pas: vous devez encapsuler à nouveau ce type de retour dans LiveData de sorte que la signature de méthode réelle aura un type de retour LiveData.

Pour commencer, accédez à PlayerDao.kt fichier et le code ci-dessous:

@Query("SELECT * FROM players WHERE id = :id")
fun getPlayer(id: Int): LiveData

Ici, vous ajoutez la possibilité de lire un lecteur de la base de données en utilisant LiveData.

Ensuite, vous allez mettre à jour PlayerRepository avec une méthode similaire qui appelle le DAO. Ouvrez le PlayerRepository.kt fichier et ajoutez le code suivant:

fun getPlayer(id: Int): LiveData {
  return playerDao.getPlayer(id)
}

Ensuite, vous appellerez le getPlayer() du repository. Pour ce faire, accédez à DetailViewModel.kt et ajoutez le code suivant:

fun getPlayer(player: PlayerListItem): LiveData {
  return repository.getPlayer(player.id)
}

Vous pouvez maintenant joindre un Observer à cet appel de méthode du DetailsFragment.kt fichier. À l’intérieur onViewCreated(), remplacer //TODO observe viewmodel changes avec le bloc de code ci-dessous:

// 1
detailViewModel.getPlayer(playerListItem).observe(viewLifecycleOwner, Observer {
  // 2
  this.player = it

  // 3
  displayPlayer()
})

Vous voilà:

  1. Ajout d’un observateur au getPlayer(playerListItem).
  2. Mise à jour du local Player avec l’objet joueur observateur it.
  3. Appel à afficher le joueur maintenant que les données de l’observateur sont à jour.

Créez et exécutez l’application. Bon travail! Les détails du joueur sont présents et l’application est presque entièrement fonctionnelle.

les détails du joueur fonctionnent

Dans la section suivante, vous commencerez à mieux comprendre la prise en charge de la coroutine dans Room en ajoutant une fonction préférée aux vues détaillées des joueurs.

Mise à jour d’un lecteur

Pour mettre à jour un joueur de tennis, utilisez la même approche que vous avez suivie dans les étapes précédentes. Il y aura à nouveau une série d’étapes pour terminer la fonctionnalité, mais ce ne sera pas si difficile cette fois. Vous avez déjà jeté les bases après tout.

Ajout d’une mise à jour au Dao

Pour commencer, ouvrez le PlayerDoa.kt déposer et ajouter le updatePlayer() comme indiqué ci-dessous:

@Update
suspend fun updatePlayer(player: Player)

Ici, vous annotez avec @Update dire à Room que updatePlayer() La méthode DAO effectuera des opérations de mise à jour. le suspend Le mot-clé dans la signature de méthode indique à Room que cette méthode DAO suspendra son exécution.

Ajout d’une mise à jour au référentiel

Ensuite, ouvrez le PlayerRespository.kt fichier et ajoutez le code suivant:

suspend fun updatePlayer(player: Player) {
  playerDao.updatePlayer(player)
}

Ici, vous avez encapsulé la fonctionnalité du DAO pour mettre à jour un Player avec une autre fonction de suspension.

Remarque: Cette étape est nécessaire car les fonctions de suspension ne peuvent s’exécuter qu’à partir d’autres fonctions de suspension ou à partir d’une coroutine.

Ajout d’une mise à jour au ViewModel

Il est temps de mettre à jour le DetailViewModel.kt pour exécuter le référentiel updatePlayer(player: Player) dans une coroutine. Tout d’abord, ajoutez la méthode suivante à la fin de DetailViewModel.kt fichier:

// 1
fun updatePlayer(player: Player) = viewModelScope.launch {
  // 2
  repository.updatePlayer(player)
}
    Ici, vous avez ajouté une nouvelle méthode pour mettre à jour le Player. Cette méthode utilise

  1. viewModelScope appeler launch, une méthode de création de coroutine
  2. Cela, à son tour, appelle updatePlayer(player: Player) dans une coroutine.

Ajout d’une mise à jour à l’interface utilisateur

Ensuite, vous allez configurer le MenuItem dans le Toolbar pour mettre à jour la sélection du joueur préféré dans la base de données. Par souci de simplicité, vous n’effectuerez des modifications que dans setupFavoriteToggle() une fonction.

Ouvrez le DetailsFragment.kt déposer et remplacer le TODO dans setupFavoriteToggle(checkBox: CheckBox, player : Player) avec ce qui suit:

// 1
checkBox.setOnCheckedChangeListener { _, b ->
  // 2
  player.favorite = b
  // 3
  detailViewModel.updatePlayer(player)
}
// 4
checkBox.isChecked = player.favorite

Vous voilà:

  1. Attachement OnCheckedChangeListener à l’étoile de la case à cocher MenuItem.
  2. Affectation de la propriété préférée du joueur à la valeur cochée de la case à cocher.
  3. Appel updatePlayer(player) de ViewModel.
  4. Gestion de la valeur initiale de checkBox.isChecked.

Maintenant, vous devez appeler cela setupFavoriteToggle méthode de l’observateur dans onViewCreated().

Ajoutez l’appel à setupFavoriteToggle, juste au dessus displayPlayer() dans la définition d’observateur comme indiqué ci-dessous:

detailViewModel.getPlayer(playerListItem).observe(this, Observer {
  this.player = it

  setupFavoriteToggle(checkbox, it) // called the method here 

  displayPlayer()
})

Ici, vous appelez la méthode qui définit la case à cocher pour les favoris.

Bon travail! Maintenant, lorsque vous quittez le DetailsFragment vous verrez une étoile remplie à côté du Player dans la liste et lorsque vous revenez à DetailsFragment l’élément de menu étoile sera également mis en surbrillance. Mieux encore, tout cela se produit en appelant pour mettre à jour le lecteur dans la base de données en utilisant la fonction de suspension du DAO et le ViewModelScopeEst la méthode de lancement pour exécuter l’opération dans une coroutine.

Générez et exécutez l’application pour observer le nouveau comportement. Vous pouvez désormais mettre en favoris vos joueurs préférés.

favori d'un joueur de tennis

Supprimer un lecteur

Les utilisateurs peuvent également vouloir supprimer des joueurs de l’application. Dans cette étape, vous allez ajouter une fonction de suppression de lecteur pour une telle occasion.

Ajout de la suppression au DAO

Pour commencer, ouvrez le PlayerDoa.kt déposer et ajouter le deletePlayer() appel de méthode:

@Delete
suspend fun deletePlayer(player: Player)

Cela devrait vous sembler familier. Vous avez ajouté une autre fonction de suspension au DAO et fourni @Delete annotation afin que Room sache que cette méthode effectuera des opérations de suppression.

Ajout de la suppression au référentiel

Maintenant, il est temps de mettre à jour le référentiel. Ouvrez le PlayerRepository.kt fichier et ajoutez le code suivant:

suspend fun deletePlayer(player: Player) {
  playerDao.deletePlayer(player)
}

Ici, vous avez ajouté une autre fonction de suspension qui utilise PlayerDao appeler le deletePlayer() de l’étape précédente.

Ajout de la suppression au ViewModel

Vous devrez mettre à jour DetailsViewModel à nouveau pour appeler le deletePlayer() méthode de repository. Ouvrez le DetailsViewModel.kt fichier et ajoutez la méthode suivante:

// 1
fun deletePlayer(player: Player) = viewModelScope.launch {
  // 2
  repository.deletePlayer(player)
}

Ici, vous faites à peu près la même chose que l’appel de méthode de mise à jour de l’implémentation de fonctionnalité précédente. Tu utilises viewModelScope appeler launch et exécutez l’opération dans une coroutine.

Wow, vous devenez un pro dans ce domaine!

Ajout de la suppression à l’interface utilisateur

Dans cette dernière étape, vous allez ajouter un comportement de suppression à l’interface utilisateur. Ouvrez le DetailsFragment.kt fichier une fois de plus et ajoutez le code suivant à l’intérieur deleteCurrentPlayer() remplacement du TODO:

detailViewModel.deletePlayer(player)
dismiss()

Ici, vous avez invoqué deletePlayer(player) sur le detailViewModel et licencié DetailsFragment.

Générez et exécutez l’application pour observer le nouveau comportement. Vous pouvez désormais supprimer tous les joueurs que vous souhaitez.

supprimer un joueur

Où aller en partant d’ici?

Bon travail! Vous êtes maintenant un Pro de Suspension-Fonction-Salle-Base de Données-Stockage! Si vous avez eu du mal à suivre, pas de soucis. Importez simplement le TennisPlayers-final version de l’application et comparez-y votre code.

Si vous avez aimé travailler avec LiveData, vous découvrirez peut-être que vous appréciez la nouvelle Couler ajout de la version 2.2. Pour plus d’informations sur Flow, consultez le notes de version du développeur.

Si vous recherchez du contenu Room ou Coroutine, lisez Room DB: Advanced Data Persistence ou Kotlin Coroutines Tutorial pour Android: Advanced pour en savoir plus.

Merci pour la lecture! Si vous avez des questions ou des commentaires, veuillez vous joindre au forum de discussion ci-dessous!

Close Menu