Principes de base d'Android en Kotlin 06.2 : Coroutines et Room

Cet atelier de programmation fait partie du cours Principes de base d'Android en Kotlin. Vous tirerez pleinement parti de ce cours en suivant les ateliers de programmation dans l'ordre. Tous les ateliers de programmation du cours sont listés sur la page de destination des ateliers de programmation Principes de base d'Android en Kotlin.

Introduction

L'une des principales priorités pour créer une expérience utilisateur parfaite pour votre application est de vous assurer que l'UI est toujours réactive et fonctionne correctement. Pour améliorer les performances de l'UI, vous pouvez déplacer les tâches de longue durée, telles que les opérations de base de données, en arrière-plan.

Dans cet atelier de programmation, vous allez implémenter la partie visible par l'utilisateur de l'application TrackMySleepQuality, en utilisant des coroutines Kotlin pour effectuer des opérations de base de données en dehors du thread principal.

Ce que vous devez déjà savoir

Vous devez maîtriser les éléments suivants :

  • Créer une interface utilisateur (UI) de base à l'aide d'une activité, de fragments, de vues et de gestionnaires de clics.
  • Naviguer entre les fragments et utiliser safeArgs pour transmettre des données simples entre les fragments.
  • Afficher les modèles, les fabriques de modèles, les transformations et LiveData.
  • Création d'une base de données Room, d'un DAO et définition d'entités
  • Il est utile de connaître les concepts de threading et de multiprocessus.

Points abordés

  • Fonctionnement des threads dans Android.
  • Comment utiliser les coroutines Kotlin pour dissocier les opérations de base de données du thread principal.
  • Comment afficher des données mises en forme dans un TextView.

Objectifs de l'atelier

  • Enrichissez l'application TrackMySleepQuality pour qu'elle puisse collecter, stocker et afficher des données dans et depuis la base de données.
  • Utilisez des coroutines pour exécuter des opérations de base de données de longue durée en arrière-plan.
  • Utilisez LiveData pour déclencher la navigation et l'affichage d'un snack-bar.
  • Utilisez LiveData pour activer et désactiver les boutons.

Dans cet atelier de programmation, vous allez créer le ViewModel, les coroutines et les parties d'affichage des données de l'application TrackMySleepQuality.

L'application comporte deux écrans, représentés par des fragments, comme illustré dans la figure ci-dessous.

Le premier écran, affiché à gauche, comporte des boutons permettant de démarrer et d'arrêter le suivi. L'écran affiche toutes les données de sommeil de l'utilisateur. Le bouton Effacer supprime définitivement toutes les données que l'application a collectées pour l'utilisateur.

Le deuxième écran, à droite, permet de sélectionner une note de qualité du sommeil. Dans l'application, la note est représentée sous forme numérique. À des fins de développement, l'application affiche à la fois les icônes de visages et leurs équivalents numériques.

Voici le parcours de l'utilisateur :

  • L'utilisateur ouvre l'application et l'écran de suivi du sommeil s'affiche.
  • L'utilisateur appuie sur le bouton Démarrer. L'heure de début est enregistrée et affichée. Le bouton Start (Démarrer) est désactivé, et le bouton Stop (Arrêter) est activé.
  • L'utilisateur appuie sur le bouton Arrêter. L'heure de fin est enregistrée et l'écran de qualité du sommeil s'ouvre.
  • L'utilisateur sélectionne une icône de qualité du sommeil. L'écran se ferme et l'écran de suivi affiche l'heure de fin du sommeil et la qualité du sommeil. Le bouton Arrêter est désactivé et le bouton Démarrer est activé. L'application est prête pour une nouvelle nuit.
  • Le bouton Effacer est activé chaque fois que la base de données contient des données. Lorsque l'utilisateur appuie sur le bouton Effacer, toutes ses données sont effacées sans possibilité de retour en arrière. Aucun message de confirmation n'est affiché.

Cette application utilise une architecture simplifiée, comme indiqué ci-dessous dans le contexte de l'architecture complète. L'application n'utilise que les composants suivants :

  • Contrôleur d'interface utilisateur
  • Afficher le modèle et LiveData
  • Une base de données Room

Dans cette tâche, vous allez utiliser un TextView pour afficher les données de suivi du sommeil mises en forme. (Il ne s'agit pas de l'interface finale. Vous découvrirez une meilleure façon de procéder dans un autre atelier de programmation.)

Vous pouvez continuer avec l'application TrackMySleepQuality que vous avez créée dans l'atelier de programmation précédent ou télécharger l'application de départ pour cet atelier de programmation.

Étape 1 : Téléchargez et exécutez l'application de démarrage

  1. Téléchargez l'application TrackMySleepQuality-Coroutines-Starter depuis GitHub.
  2. Créez et exécutez l'application. L'application affiche l'UI du fragment SleepTrackerFragment, mais aucune donnée. Les boutons ne répondent pas aux commandes tactiles.

Étape 2 : Inspecter le code

Le code de démarrage de cet atelier de programmation est identique au code de solution de l'atelier de programmation 6.1 Créer une base de données Room.

  1. Ouvrez res/layout/activity_main.xml. Cette mise en page contient le fragment nav_host_fragment. Notez également la balise <merge>.

    La balise merge peut être utilisée pour éliminer les mises en page redondantes lors de l'inclusion de mises en page. Il est recommandé de l'utiliser. Un exemple de mise en page redondante serait ConstraintLayout > LinearLayout > TextView, où le système pourrait être en mesure d'éliminer le LinearLayout. Ce type d'optimisation peut simplifier la hiérarchie des vues et améliorer les performances de l'application.
  2. Dans le dossier navigation, ouvrez navigation.xml. Vous pouvez voir deux fragments et les actions de navigation qui les relient.
  3. Dans le dossier layout, double-cliquez sur le fragment du suivi du sommeil pour afficher sa mise en page XML. Notez les points suivants :
  • Les données de mise en page sont encapsulées dans un élément <layout> pour activer la liaison de données.
  • ConstraintLayout et les autres vues sont organisées dans l'élément <layout>.
  • Le fichier comporte un espace réservé pour la balise <data>.

L'application de démarrage fournit également des dimensions, des couleurs et des styles pour l'UI. L'application contient une base de données Room, un DAO et une entité SleepNight. Si vous n'avez pas terminé le codelab précédent, assurez-vous d'explorer vous-même ces aspects du code.

Maintenant que vous disposez d'une base de données et d'une UI, vous devez collecter des données, les ajouter à la base de données et les afficher. Tout ce travail est effectué dans le modèle de vue. Le ViewModel du suivi du sommeil gérera les clics sur les boutons, interagira avec la base de données via le DAO et fournira des données à l'UI via LiveData. Toutes les opérations de base de données devront être exécutées hors du thread UI principal. Pour ce faire, vous allez devoir utiliser des coroutines.

Étape 1 : Ajouter SleepTrackerViewModel

  1. Dans le package sleeptracker, ouvrez SleepTrackerViewModel.kt.
  2. Inspectez la classe SleepTrackerViewModel, qui vous est fournie dans l'application de démarrage et qui est également présentée ci-dessous. Notez que la classe étend AndroidViewModel(). Cette classe est identique à ViewModel, mais elle accepte le contexte d'application en tant que paramètre et le rend disponible en tant que propriété. Vous en aurez besoin ultérieurement.
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

Étape 2 : Ajouter SleepTrackerViewModelFactory

  1. Dans le package sleeptracker, ouvrez SleepTrackerViewModelFactory.kt.
  2. Examinez le code qui vous est fourni pour la fabrique, comme indiqué ci-dessous :
class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

Veuillez noter les points suivants :

  • Le SleepTrackerViewModelFactory fourni accepte le même argument que ViewModel et étend ViewModelProvider.Factory.
  • Dans la fabrique, le code remplace create(), qui accepte n'importe quel type de classe comme argument et renvoie un ViewModel.
  • Dans le corps de create(), le code vérifie qu'une classe SleepTrackerViewModel est disponible et, le cas échéant, renvoie une instance de celle-ci. Sinon, le code génère une exception.

Étape 3 : Mettez à jour SleepTrackerFragment

  1. Dans SleepTrackerFragment, obtenez une référence au contexte de l'application. Placez la référence dans onCreateView(), sous binding. Vous avez besoin d'une référence à l'application à laquelle ce fragment est associé pour la transmettre au fournisseur de fabrique de ViewModel.

    La fonction Kotlin requireNotNull génère une IllegalArgumentException si la valeur est null.
val application = requireNotNull(this.activity).application
  1. Vous avez besoin d'une référence à votre source de données via une référence au DAO. Dans onCreateView(), avant return, définissez un dataSource. Pour obtenir une référence au DAO de la base de données, utilisez SleepDatabase.getInstance(application).sleepDatabaseDao.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. Dans onCreateView(), avant return, créez une instance de viewModelFactory. Vous devez lui transmettre dataSource et application.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. Maintenant que vous disposez d'une fabrique, obtenez une référence à SleepTrackerViewModel. Le paramètre SleepTrackerViewModel::class.java fait référence à la classe Java d'exécution de cet objet.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. Votre code, une fois fini, doit ressembler à ceci :
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

Voici la méthode onCreateView() jusqu'à présent :

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)

        val application = requireNotNull(this.activity).application

        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

        val sleepTrackerViewModel =
                ViewModelProviders.of(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

Étape 4 : Ajoutez la liaison de données pour le modèle de vue

Maintenant que le ViewModel de base est en place, vous devez terminer la configuration de la liaison de données dans le SleepTrackerFragment pour connecter le ViewModel à l'UI.


Dans le fichier de mise en page fragment_sleep_tracker.xml :

  1. Dans le bloc <data>, créez un <variable> qui fait référence à la classe SleepTrackerViewModel.
<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

Dans SleepTrackerFragment :

  1. Définissez l'activité actuelle comme propriétaire du cycle de vie de la liaison. Ajoutez ce code dans la méthode onCreateView(), avant l'instruction return :
binding.setLifecycleOwner(this)
  1. Attribuez la variable de liaison sleepTrackerViewModel à sleepTrackerViewModel. Insérez ce code dans onCreateView(), sous le code qui crée SleepTrackerViewModel :
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. Une erreur s'affichera probablement, car vous devez recréer l'objet de liaison. Nettoyez et recompilez le projet pour éliminer l'erreur.
  2. Enfin, comme toujours, assurez-vous que votre code se compile et s'exécute sans erreur.

En langage Kotlin, les coroutines permettent de gérer les tâches de longue durée de manière élégante et efficace. Les coroutines Kotlin permettent de convertir en code séquentiel un code basé sur des rappels. Le code écrit de manière séquentielle est généralement plus facile à lire et peut même utiliser des fonctionnalités de langage telles que les exceptions. Au final, les coroutines et les rappels remplissent la même fonction : attendre que le résultat d'une tâche de longue durée soit disponible et poursuivre l'exécution.

Les coroutines possèdent les propriétés suivantes :

  • Les coroutines sont asynchrones et non bloquantes.
  • Les coroutines utilisent des fonctions suspend pour rendre le code asynchrone séquentiel.

Les coroutines sont asynchrones.

Une coroutine s'exécute indépendamment des étapes d'exécution principales de votre programme. Cela peut se faire en parallèle ou sur un processeur distinct. Il est également possible que vous effectuiez un peu de traitement pendant que le reste de l'application attend une entrée. L'un des aspects importants de l'asynchrone est que vous ne pouvez pas vous attendre à ce que le résultat soit disponible tant que vous ne l'avez pas explicitement attendu.

Par exemple, imaginons que vous ayez une question qui nécessite des recherches et que vous demandiez à un collègue de trouver la réponse. Ils s'en occupent, ce qui revient à dire qu'ils effectuent le travail de manière "asynchrone" et "sur un thread distinct". Vous pouvez continuer à effectuer d'autres tâches qui ne dépendent pas de la réponse, jusqu'à ce que votre collègue revienne et vous la communique.

Les coroutines ne sont pas bloquantes.

Non bloquant signifie qu'une coroutine ne bloque pas le thread principal ni le thread de l'UI. Ainsi, avec les coroutines, les utilisateurs bénéficient toujours de l'expérience la plus fluide possible, car l'interaction avec l'UI est toujours prioritaire.

Les coroutines utilisent des fonctions suspend pour rendre le code asynchrone séquentiel.

Le mot clé suspend permet à Kotlin de marquer une fonction ou un type de fonction comme étant disponible pour les coroutines. Lorsqu'une coroutine appelle une fonction marquée avec suspend, au lieu de se bloquer jusqu'à ce que la fonction renvoie un résultat comme un appel de fonction normal, la coroutine suspend l'exécution jusqu'à ce que le résultat soit prêt. La coroutine reprend ensuite là où elle s'était arrêtée, avec le résultat.

Pendant que la coroutine est suspendue et attend un résultat, elle débloque le thread sur lequel elle s'exécute. Cela permet à d'autres fonctions ou coroutines de s'exécuter.

Le mot clé suspend ne spécifie pas le thread sur lequel le code s'exécute. Une fonction de suspension peut s'exécuter sur un thread d'arrière-plan ou sur le thread principal.

Pour utiliser des coroutines en Kotlin, vous avez besoin de trois éléments :

  • Un emploi
  • Un coordinateur
  • Un champ d'application

Tâche : une tâche est tout ce qui peut être annulé. Chaque coroutine possède une tâche, et vous pouvez utiliser cette tâche pour annuler la coroutine. Les tâches peuvent être organisées en hiérarchies parent-enfant. L'annulation d'un job parent annule immédiatement tous les jobs enfants, ce qui est beaucoup plus pratique que d'annuler chaque coroutine manuellement.

Coordinateur  : le coordinateur envoie des coroutines à exécuter sur différents threads. Par exemple, Dispatcher.Main exécute des tâches sur le thread principal, et Dispatcher.IO décharge les tâches d'E/S bloquantes dans un pool de threads partagé.

Champ d'application  : le champ d'application d'une coroutine définit le contexte dans lequel la coroutine s'exécute. Un scope combine des informations sur le répartiteur et le job d'une coroutine. Les coroutines sont suivies par les scopes. Lorsque vous lancez une coroutine, elle se trouve "dans un champ d'application", ce qui signifie que vous avez indiqué quel champ d'application suivra la coroutine.

Vous souhaitez que l'utilisateur puisse interagir avec les données de sommeil de différentes manières :

  • Lorsque l'utilisateur appuie sur le bouton Start (Démarrer), l'application crée une nuit de sommeil et la stocke dans la base de données.
  • Lorsque l'utilisateur appuie sur le bouton Arrêter, l'application met à jour la nuit avec une heure de fin.
  • Lorsque l'utilisateur appuie sur le bouton Effacer, l'application efface les données de la base de données.

Ces opérations sur la base de données peuvent prendre beaucoup de temps. Elles doivent donc s'exécuter sur un thread distinct.

Étape 1 : Configurer des coroutines pour les opérations de base de données

Lorsque l'utilisateur appuie sur le bouton Start (Démarrer) de l'application Sleep Tracker, vous devez appeler une fonction dans SleepTrackerViewModel pour créer une instance de SleepNight et la stocker dans la base de données.

Appuyer sur l'un des boutons déclenche une opération de base de données, comme la création ou la mise à jour d'un SleepNight. Pour cette raison et d'autres, vous utilisez des coroutines pour implémenter des gestionnaires de clics pour les boutons de l'application.

  1. Ouvrez le fichier build.gradle au niveau de l'application et recherchez les dépendances pour les coroutines. Pour utiliser des coroutines, vous avez besoin de ces dépendances, qui ont été ajoutées pour vous.

    $coroutine_version est défini dans le fichier build.gradle du projet en tant que coroutine_version = '1.0.0'.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
  1. Ouvrez le fichier SleepTrackerViewModel.
  2. Dans le corps de la classe, définissez viewModelJob et attribuez-lui une instance de Job. Ce viewModelJob vous permet d'annuler toutes les coroutines démarrées par ce ViewModel lorsque le ViewModel n'est plus utilisé et est détruit. Vous évitez ainsi de vous retrouver avec des coroutines qui n'ont nulle part où revenir.
private var viewModelJob = Job()
  1. À la fin du corps de la classe, remplacez onCleared() et annulez toutes les coroutines. Lorsque le ViewModel est détruit, onCleared() est appelé.
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. Juste en dessous de la définition de viewModelJob, définissez un uiScope pour les coroutines. Le scope détermine le thread sur lequel la coroutine s'exécutera. Il doit également connaître le job. Pour obtenir un scope, demandez une instance de CoroutineScope et transmettez un répartiteur et un job.

L'utilisation de Dispatchers.Main signifie que les coroutines lancées dans uiScope s'exécuteront sur le thread principal. Cela est logique pour de nombreuses coroutines démarrées par un ViewModel, car après avoir effectué un traitement, elles entraînent une mise à jour de l'UI.

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
  1. Sous la définition de uiScope, définissez une variable appelée tonight pour contenir la nuit en cours. Définissez la variable MutableLiveData, car vous devez pouvoir observer les données et les modifier.
private var tonight = MutableLiveData<SleepNight?>()
  1. Pour initialiser la variable tonight dès que possible, créez un bloc init sous la définition de tonight et appelez initializeTonight(). Vous définirez initializeTonight() à l'étape suivante.
init {
   initializeTonight()
}
  1. Sous le bloc init, implémentez initializeTonight(). Dans uiScope, lancez une coroutine. À l'intérieur, récupérez la valeur de tonight à partir de la base de données en appelant getTonightFromDatabase(), puis attribuez la valeur à tonight.value. Vous définirez getTonightFromDatabase() à l'étape suivante.
private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. Mettre en œuvre getTonightFromDatabase() Définissez-le comme une fonction private suspend qui renvoie un SleepNight pouvant être nul, s'il n'y a pas de SleepNight en cours. Un message d'erreur s'affiche, car la fonction doit renvoyer quelque chose.
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. Dans le corps de la fonction getTonightFromDatabase(), renvoyez le résultat d'une coroutine qui s'exécute dans le contexte Dispatchers.IO. Utilisez le répartiteur d'E/S, car l'obtention de données à partir de la base de données est une opération d'E/S et n'a rien à voir avec l'UI.
  return withContext(Dispatchers.IO) {}
  1. Dans le bloc de retour, laissez la coroutine obtenir la nuit (la plus récente) à partir de la base de données. Si les heures de début et de fin ne sont pas identiques, ce qui signifie que la nuit est déjà terminée, renvoyez null. Sinon, renvoie la nuit.
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night

Votre fonction suspendue getTonightFromDatabase() terminée devrait se présenter comme suit. Il ne devrait plus y avoir d'erreurs.

private suspend fun getTonightFromDatabase(): SleepNight? {
   return withContext(Dispatchers.IO) {
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night
   }
}

Étape 2 : Ajoutez le gestionnaire de clics pour le bouton "Démarrer"

Vous pouvez maintenant implémenter onStartTracking(), le gestionnaire de clics pour le bouton Start (Démarrer). Vous devez créer un SleepNight, l'insérer dans la base de données et l'attribuer à tonight. La structure de onStartTracking() ressemblera beaucoup à celle de initializeTonight().

  1. Commencez par la définition de la fonction pour onStartTracking(). Vous pouvez placer les gestionnaires de clics au-dessus de onCleared() dans le fichier SleepTrackerViewModel.
fun onStartTracking() {}
  1. Dans onStartTracking(), lancez une coroutine dans uiScope, car vous avez besoin de ce résultat pour continuer et mettre à jour l'UI.
uiScope.launch {}
  1. Dans le lancement de la coroutine, créez un SleepNight qui capture l'heure actuelle comme heure de début.
        val newNight = SleepNight()
  1. Toujours dans le lancement de la coroutine, appelez insert() pour insérer newNight dans la base de données. Une erreur s'affiche, car vous n'avez pas encore défini cette fonction de suspension insert(). (Il ne s'agit pas de la fonction DAO du même nom.)
       insert(newNight)
  1. Mettez également à jour tonight dans le lancement de la coroutine.
       tonight.value = getTonightFromDatabase()
  1. Sous onStartTracking(), définissez insert() comme fonction private suspend qui reçoit un SleepNight comme argument.
private suspend fun insert(night: SleepNight) {}
  1. Pour le corps de insert(), lancez une coroutine dans le contexte d'E/S et insérez la nuit dans la base de données en appelant insert() à partir du DAO.
   withContext(Dispatchers.IO) {
       database.insert(night)
   }
  1. Dans le fichier de mise en page fragment_sleep_tracker.xml, ajoutez le gestionnaire de clics pour onStartTracking() à start_button à l'aide de la liaison de données que vous avez configurée précédemment. La notation de la fonction @{() -> crée une fonction lambda qui ne prend aucun argument et appelle le gestionnaire de clics dans sleepTrackerViewModel.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. Compilez et exécutez votre application. Appuyez sur le bouton Start (Démarrer). Cette action crée des données, mais pour l'instant, vous ne voyez rien. Vous allez le résoudre ci-après.
fun someWorkNeedsToBeDone {
   uiScope.launch {

        suspendFunction()

   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

Étape 3 : Affichez les données

Dans SleepTrackerViewModel, la variable nights fait référence à LiveData, car getAllNights() dans le DAO renvoie LiveData.

Il s'agit d'une fonctionnalité Room qui permet de mettre à jour le nights LiveData chaque fois que les données de la base de données changent, afin d'afficher les dernières données. Vous n'avez jamais besoin de définir explicitement LiveData ni de le mettre à jour. Room met à jour les données pour qu'elles correspondent à la base de données.

Toutefois, si vous affichez nights dans une vue de texte, la référence de l'objet s'affiche. Pour afficher le contenu de l'objet, transformez les données en chaîne mise en forme. Utilisez une carte Transformation qui est exécutée chaque fois que nights reçoit de nouvelles données de la base de données.

  1. Ouvrez le fichier Util.kt et annulez la mise en commentaire du code pour la définition de formatNights() et des instructions import associées. Pour décommenter du code dans Android Studio, sélectionnez tout le code marqué avec // et appuyez sur Cmd+/ ou Control+/.
  2. Notez que formatNights() renvoie un type Spanned, qui est une chaîne au format HTML.
  3. Ouvrez strings.xml. Notez l'utilisation de CDATA pour mettre en forme les ressources de chaîne afin d'afficher les données de sommeil.
  4. Ouvrez SleepTrackerViewModel. Dans la classe SleepTrackerViewModel, sous la définition de uiScope, définissez une variable appelée nights. Récupérez toutes les nuits de la base de données et attribuez-les à la variable nights.
private val nights = database.getAllNights()
  1. Juste en dessous de la définition de nights, ajoutez du code pour transformer nights en nightsString. Utilisez la fonction formatNights() à partir de Util.kt.

    Transmettez nights à la fonction map() à partir de la classe Transformations. Pour accéder à vos ressources de chaîne, définissez la fonction de mappage comme appelant formatNights(). Fournissez nights et un objet Resources.
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. Ouvrez le fichier de mise en page fragment_sleep_tracker.xml. Dans TextView, dans la propriété android:text, vous pouvez maintenant remplacer la chaîne de ressource par une référence à nightsString.
"@{sleepTrackerViewModel.nightsString}"
  1. Recompilez votre code et exécutez l'application. Toutes vos données de sommeil avec les heures de début devraient maintenant s'afficher.
  2. Appuyez plusieurs fois sur le bouton Démarrer pour afficher plus de données.

À l'étape suivante, vous allez activer la fonctionnalité du bouton Stop (Arrêter).

Étape 4 : Ajoutez le gestionnaire de clics pour le bouton "Stop" (Arrêter)

En utilisant le même modèle que lors de l'étape précédente, implémentez le gestionnaire de clics pour le bouton Stop dans SleepTrackerViewModel..

  1. Ajoutez onStopTracking() à ViewModel. Lancez une coroutine dans uiScope. Si l'heure de fin n'a pas encore été définie, définissez endTimeMilli sur l'heure système actuelle et appelez update() avec les données de nuit.

    En Kotlin, la syntaxe return@label spécifie la fonction à partir de laquelle cette instruction renvoie, parmi plusieurs fonctions imbriquées.
fun onStopTracking() {
   uiScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. Implémentez update() en utilisant le même modèle que pour implémenter insert().
private suspend fun update(night: SleepNight) {
   withContext(Dispatchers.IO) {
       database.update(night)
   }
}
  1. Pour connecter le gestionnaire de clics à l'UI, ouvrez le fichier de mise en page fragment_sleep_tracker.xml et ajoutez le gestionnaire de clics à stop_button.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. Créez et exécutez votre application.
  2. Appuyez sur Démarrer, puis sur Arrêter. Vous voyez l'heure de début, l'heure de fin, la qualité du sommeil sans valeur et la durée du sommeil.

Étape 5 : Ajoutez le gestionnaire de clics pour le bouton "Effacer"

  1. De même, implémentez onClear() et clear().
fun onClear() {
   uiScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
   withContext(Dispatchers.IO) {
       database.clear()
   }
}
  1. Pour connecter le gestionnaire de clics à l'UI, ouvrez fragment_sleep_tracker.xml et ajoutez le gestionnaire de clics à clear_button.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. Créez et exécutez votre application.
  2. Appuyez sur Effacer pour supprimer toutes les données. Appuyez ensuite sur Démarrer et Arrêter pour générer de nouvelles données.

Projet Android Studio : TrackMySleepQualityCoroutines

  • Utilisez ViewModel, ViewModelFactory et la liaison de données pour configurer l'architecture de l'UI de l'application.
  • Pour que l'interface utilisateur fonctionne de manière fluide, utilisez des coroutines pour les longues tâches, telles que les opérations de base de données.
  • Les coroutines sont asynchrones et non bloquantes. Elles utilisent des fonctions suspend pour rendre le code asynchrone séquentiel.
  • Lorsqu'une coroutine appelle une fonction marquée avec suspend, au lieu de se bloquer jusqu'à ce que cette fonction renvoie un résultat comme un appel de fonction normal, elle suspend son exécution jusqu'à ce que le résultat soit prêt. Il reprend ensuite là où il s'était arrêté avec le résultat.
  • La différence entre bloquer et suspendre est que si un thread est bloqué, aucune autre tâche n'est effectuée. Si le thread est suspendu, d'autres tâches sont effectuées jusqu'à ce que le résultat soit disponible.

Pour lancer une coroutine, vous avez besoin d'un job, d'un répartiteur et d'un champ d'application :

  • En gros, un job est tout ce qui peut être annulé. Chaque coroutine possède une tâche, et vous pouvez utiliser une tâche pour annuler une coroutine.
  • Le coordinateur envoie des coroutines à exécuter sur différents threads. Dispatcher.Main exécute les tâches sur le thread principal, tandis que Dispartcher.IO permet de décharger les tâches d'E/S bloquantes dans un pool de threads partagé.
  • Le champ d'application combine des informations, y compris une tâche et un répartiteur, pour définir le contexte dans lequel la coroutine s'exécute. Les coroutines sont suivies par les scopes.

Pour implémenter des gestionnaires de clics qui déclenchent des opérations de base de données, suivez ce modèle :

  1. Lancez une coroutine qui s'exécute sur le thread principal ou de l'UI, car le résultat affecte l'UI.
  2. Appelez une fonction de suspension pour effectuer le travail de longue durée, afin de ne pas bloquer le thread UI en attendant le résultat.
  3. Le travail de longue durée n'a rien à voir avec l'UI. Passez donc au contexte d'E/S. Ainsi, le travail peut s'exécuter dans un pool de threads optimisé et réservé à ce type d'opérations.
  4. Appelez ensuite la fonction de base de données pour effectuer le travail.

Utilisez une carte Transformations pour créer une chaîne à partir d'un objet LiveData chaque fois que l'objet change.

Cours Udacity :

Documentation pour les développeurs Android :

Autres articles et documentation :

Cette section répertorie les devoirs possibles pour les élèves qui suivent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. Il revient à l'enseignant d'effectuer les opérations suivantes :

  • Attribuer des devoirs si nécessaire
  • Indiquer aux élèves comment rendre leurs devoirs
  • Noter les devoirs

Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ne doivent pas hésiter à attribuer d'autres devoirs aux élèves s'ils le jugent nécessaire.

Si vous suivez cet atelier de programmation par vous-même, n'hésitez pas à utiliser ces devoirs pour tester vos connaissances.

Répondez à ces questions

Question 1

Voici quelques avantages des coroutines :

  • Elles ne sont pas bloquantes.
  • Elles s'exécutent de manière asynchrone.
  • Elles peuvent être exécutées sur un autre thread que le thread principal.
  • Elles rendent toujours l'application plus rapide.
  • Elles peuvent utiliser des exceptions.
  • Elles peuvent être écrites et lues en tant que code linéaire.

Question 2

Qu'est-ce qu'une fonction de suspension ?

  • Une fonction ordinaire annotée avec le mot clé suspend.
  • Une fonction pouvant être appelée dans des coroutines.
  • Lorsqu'une fonction de suspension est en cours d'exécution, le thread d'appel est suspendu.
  • Les fonctions de suspension doivent toujours s'exécuter en arrière-plan.

Question 3

Quelle est la différence entre bloquer et suspendre un fil de discussion ? Sélectionnez toutes les réponses qui s'appliquent.

  • Lorsque l'exécution est bloquée, aucune autre tâche ne peut être exécutée sur le thread bloqué.
  • Lorsque l'exécution est suspendue, le thread peut effectuer d'autres tâches en attendant que le travail déchargé soit terminé.
  • La suspension est plus efficace, car les threads ne risquent pas d'attendre sans rien faire.
  • Qu'elle soit bloquée ou suspendue, l'exécution attend toujours le résultat de la coroutine avant de continuer.

Passer à la leçon suivante : 6.3 Utiliser LiveData pour contrôler les états des boutons

Pour obtenir des liens vers d'autres ateliers de programmation de ce cours, consultez la page de destination des ateliers de programmation Principes de base d'Android en Kotlin.