Créer une bibliothèque d'extensions Kotlin

Android KTX est un ensemble d'extensions destinées, entre autres, aux API courantes du framework Android et aux bibliothèques Android Jetpack. Nous avons créé ces extensions pour que l'appel d'API basées sur le langage de programmation Java à partir de code Kotlin puisse s'effectuer de manière plus concise et idiomatique. Pour cela, nous exploitons des fonctionnalités linguistiques telles que les fonctions et les propriétés d'extension, les lambdas, les paramètres nommés et par défaut, ainsi que les coroutines.

Qu'est-ce qu'une bibliothèque KTX ?

KTX signifie "Kotlin extensions" (extensions Kotlin). Il ne s'agit pas d'une technologie ni d'une fonctionnalité linguistique particulière du langage Kotlin. Il s'agit simplement du nom que nous donnons aux bibliothèques Kotlin de Google qui prolongent les fonctionnalités des API initialement développées dans le langage de programmation Java.

L'avantage des extensions Kotlin est que n'importe quel développeur peut créer sa propre bibliothèque pour ses API, voire pour des bibliothèques tierces qu'il utilise dans ses projets.

Cet atelier de programmation présente quelques exemples d'ajout d'extensions simples qui exploitent les fonctionnalités du langage Kotlin. Nous verrons également comment convertir un appel asynchrone dans une API basée sur le rappel en fonction de suspension et en Flow, autrement dit en un flux asynchrone basé sur des coroutines.

Objectif de cet atelier

Dans cet atelier de programmation, vous allez travailler sur une application simple qui obtient et affiche la position actuelle de l'utilisateur. Cette appli pourra :

  • obtenir la dernière position connue auprès du fournisseur de données de localisation ;
  • s'inscrire pour recevoir des mises à jour en temps réel de la position de l'utilisateur en cours d'exécution ;
  • afficher la position à l'écran et gérer les états d'erreur si cette position n'est pas disponible.

Points abordés

  • Ajouter des extensions Kotlin aux classes existantes
  • Convertir un appel asynchrone renvoyant un résultat unique en une fonction de suspension de coroutine
  • Utiliser Flow pour obtenir des données provenant d'une source qui peut émettre une valeur plusieurs fois

Prérequis

  • Version récente d'Android Studio (version 3.6 ou ultérieure recommandée)
  • Android Emulator ou appareil connecté via USB
  • Connaissance de base du développement Android et du langage Kotlin
  • Connaissance de base des coroutines et des fonctions de suspension

Télécharger le code

Cliquez sur le lien ci-dessous pour télécharger l'ensemble du code de cet atelier de programmation :

Télécharger le code source

Vous pouvez également cloner le dépôt GitHub à partir de la ligne de commande à l'aide de la commande suivante :

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

Le code de cet atelier de programmation se trouve dans le répertoire ktx-library-codelab.

Dans le répertoire du projet, vous trouverez plusieurs dossiers step-NN contenant l'état final souhaité de chaque étape de cet atelier de programmation, à titre de référence.

Tout le travail de codage sera effectué dans le répertoire work.

Exécuter l'application pour la première fois

Ouvrez le dossier racine (ktx-library-codelab) dans Android Studio, puis sélectionnez la configuration d'exécution work-app dans la liste déroulante, comme illustré ci-dessous :

79c2a2d2f9bbb388.png

Appuyez sur le bouton Exécuter 35a622f38049c660.png pour tester votre application :

58b6a81af969abf0.png

Cette appli ne fait rien d'intéressant pour l'instant. Il lui manque quelques éléments pour qu'elle puisse afficher des données. Nous ajouterons les fonctionnalités manquantes au cours des étapes suivantes.

Vérifier plus facilement les autorisations

58b6a81af969abf0.png

Même si l'application s'exécute, elle affiche seulement un message d'erreur : elle n'est pas en mesure d'obtenir la position actuelle.

En effet, il lui manque le code permettant de demander à l'utilisateur une autorisation d'accéder à la position en cours d'exécution.

Ouvrez MainActivity.kt, puis recherchez le code commenté ci-dessous :

//  val permissionApproved = ActivityCompat.checkSelfPermission(
//      this,
//      Manifest.permission.ACCESS_FINE_LOCATION
//  ) == PackageManager.PERMISSION_GRANTED
//  if (!permissionApproved) {
//      requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 0)
//  }

Si vous annulez la mise en commentaire du code et exécutez l'application, celle-ci demandera l'autorisation et affichera la position. Ce code est toutefois difficile à lire, pour plusieurs raisons :

  • Il utilise une méthode statique checkSelfPermission de la classe utilitaire ActivityCompat, qui ne sert qu'à contenir des méthodes assurant la rétrocompatibilité.
  • La méthode utilise toujours une instance Activity comme premier paramètre, car il est impossible d'ajouter une méthode à une classe de framework dans le langage de programmation Java.
  • Notre but est de vérifier si l'autorisation est définie sur PERMISSION_GRANTED. Il est donc préférable d'utiliser directement un opérateur booléen qui aurait la valeur true si l'autorisation est accordée et la valeur "false" dans le cas contraire.

Nous souhaitons raccourcir le code détaillé présenté ci-dessus pour obtenir ce qui suit :

if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
    // request permission
}

Nous allons raccourcir le code en appliquant une fonction d'extension à Activity. Dans le projet, vous trouverez un autre module nommé myktxlibrary. Ouvrez ActivityUtils.kt à partir de ce module, puis ajoutez la fonction suivante :

fun Activity.hasPermission(permission: String): Boolean {
    return ActivityCompat.checkSelfPermission(
        this,
        permission
    ) == PackageManager.PERMISSION_GRANTED
}

Examinons cette fonction plus en détail :

  • L'utilisation de fun dans la portée la plus externe (et non à l'intérieur d'une class) signifie que nous définissons une fonction de niveau supérieur dans le fichier.
  • Activity.hasPermission définit une fonction d'extension nommée hasPermission sur un récepteur de type Activity.
  • Elle utilise l'autorisation en tant qu'argument String et renvoie une valeur Boolean qui indique si l'autorisation a été accordée.

Mais qu'est-ce qu'un récepteur de type X ?

Vous rencontrerez très souvent ce terme dans la documentation sur les fonctions d'extension Kotlin. Il signifie que cette fonction est toujours appelée sur une instance Activity (dans notre cas) ou ses sous-classes. Dans le corps de la fonction, nous pouvons faire référence à cette instance à l'aide du mot clé this (qui peut également être implicite, ce qui signifie que nous pouvons l'omettre complètement).

C'est là tout l'intérêt des fonctions d'extension : ajouter de nouvelles fonctionnalités à une classe que nous ne pouvons ou ne souhaitons pas modifier.

Voyons comment appeler cette fonction dans notre fichier MainActivity.kt. Ouvrez-le et remplacez le code concernant les autorisations par ce qui suit :

if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
   requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 0)
}

Si vous exécutez l'application maintenant, vous pouvez voir la position affichée à l'écran.

C040ceb7a6bfb27b.png

Mettre en forme le texte qui indique la position

Le texte qui indique la position est loin d'être satisfaisant. Il utilise la méthode Location.toString par défaut, qui n'est pas conçue pour s'afficher dans une interface utilisateur.

Ouvrez la classe LocationUtils.kt dans myktxlibrary. Ce fichier contient des extensions pour la classe Location. Complétez la fonction d'extension Location.format de façon à renvoyer une chaîne String mise en forme, puis modifiez Activity.showLocation dans ActivityUtils.kt pour utiliser l'extension.

Si vous rencontrez des difficultés, vous pouvez vous référer au code figurant dans le dossier step-03. Le résultat final doit se présenter comme suit :

b8ef64975551f2a.png

API Fused Location Provider des services Google Play

Le projet d'application sur lequel nous travaillons utilise l'API Fused Location Provider des services Google Play pour obtenir des données de localisation. L'API elle-même est assez simple, mais comme l'obtention de la position d'un utilisateur n'est pas une opération instantanée, tous les appels à la bibliothèque doivent être asynchrones, ce qui complique notre code avec des rappels.

Pour obtenir la position d'un utilisateur, deux étapes sont nécessaires. La première étape consiste à obtenir la dernière position connue, si elle est disponible. La seconde étape consiste à recevoir des mises à jour périodiques de la position lorsque l'appli est en cours d'exécution.

Obtenir la dernière position connue

Dans Activity.onCreate, nous allons initialiser FusedLocationProviderClient, qui sera notre point d'entrée dans la bibliothèque.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
}

Dans Activity.onStart, nous allons ensuite appeler getLastKnownLocation(), qui se présente actuellement comme suit :

private fun getLastKnownLocation() {
   fusedLocationClient.lastLocation.addOnSuccessListener { lastLocation ->
       showLocation(R.id.textView, lastLocation)
   }.addOnFailureListener { e ->
       findAndSetText(R.id.textView, "Unable to get location.")
       e.printStackTrace()
   }
}

Comme vous pouvez le voir, lastLocation est un appel asynchrone pouvant se terminer par une réussite ou un échec. Pour chacun de ces résultats, nous devons enregistrer une fonction de rappel qui va soit indiquer la position dans l'interface utilisateur, soit afficher un message d'erreur.

Ce code ne semble pas très compliqué à cause des rappels pour l'instant, mais dans un projet réel, vous voudrez peut-être traiter la position, l'enregistrer dans une base de données ou l'importer sur un serveur. La plupart de ces opérations sont également asynchrones. L'accumulation de rappels finirait par rendre le code illisible, un peu comme ceci :

private fun getLastKnownLocation() {
   fusedLocationClient.lastLocation.addOnSuccessListener { lastLocation ->
       getLastLocationFromDB().addOnSuccessListener {
           if (it != location) {
               saveLocationToDb(location).addOnSuccessListener {
                   showLocation(R.id.textView, lastLocation)
               }
           }
       }.addOnFailureListener { e ->
           findAndSetText(R.id.textView, "Unable to read location from DB.")
           e.printStackTrace()
       }
   }.addOnFailureListener { e ->
       findAndSetText(R.id.textView, "Unable to get location.")
       e.printStackTrace()
   }
}

Pire encore, le code ci-dessus pose des problèmes de fuites de mémoire et d'opérations, car les écouteurs ne sont jamais supprimés une fois que l'objet Activity qui les contient est terminé.

Nous allons rechercher un moyen plus efficace de résoudre ces problèmes en utilisant des coroutines, qui permettent de rédiger du code asynchrone semblable à un bloc de code impératif standard suivant une approche descendante, sans effectuer d'appels bloquants dans le thread d'appel. De plus, les coroutines peuvent être annulées, ce qui nous permet de procéder à un nettoyage dès qu'elles dépassent la portée définie.

À l'étape suivante, nous ajouterons une fonction d'extension qui convertit l'API de rappel existante en une fonction de suspension qui peut être appelée depuis la portée d'une coroutine associée à votre interface utilisateur. Le résultat final doit ressembler à l'exemple suivant :

private fun getLastKnownLocation() {
    try {
        val lastLocation = fusedLocationClient.awaitLastLocation();
        // process lastLocation here if needed
        showLocation(R.id.textView, lastLocation)
    } (e: Exception) {
        // we can do regular exception handling here or let it throw outside the function
    }
}

Créer une fonction de suspension à l'aide de suspendCancellableCoroutine

Ouvrez LocationUtils.kt et définissez une nouvelle fonction d'extension sur FusedLocationProviderClient :

suspend fun FusedLocationProviderClient.awaitLastLocation(): Location =
   suspendCancellableCoroutine { continuation ->
    TODO("Return results from the lastLocation call here")
}

Avant de passer à l'implémentation, examinons plus en détail le prototype de cette fonction :

  • Vous connaissez déjà la fonction d'extension et le type de récepteur grâce aux sections précédentes de cet atelier de programmation : fun FusedLocationProviderClient.awaitLastLocation.
  • suspend signifie qu'il s'agit d'une fonction de suspension, un type spécial de fonction qui ne peut être appelé que dans une coroutine ou à partir d'une autre fonction suspend.
  • Le type de résultat produit par l'appel de cette fonction est Location, comme s'il s'agissait d'un moyen synchrone d'obtenir un résultat de localisation de l'API.

Pour créer le résultat, nous allons utiliser suspendCancellableCoroutine, un bloc fonctionnel de bas niveau permettant de créer des fonctions de suspension à partir de la bibliothèque de coroutines.

suspendCancellableCoroutine exécute le bloc de code qui lui est transmis en tant que paramètre, puis suspend l'exécution de la coroutine en attendant un résultat.

Essayons d'ajouter les rappels en cas de réussite et d'échec au corps de la fonction, comme nous l'avons vu avec l'appel lastLocation précédent. Malheureusement, comme vous pouvez le constater dans les commentaires ci-dessous, il nous est impossible de renvoyer un résultat dans le corps du rappel :

suspend fun FusedLocationProviderClient.awaitLastLocation(): Location =
   suspendCancellableCoroutine { continuation ->
    lastLocation.addOnSuccessListener { location ->
        // this is not allowed here:
        // return location
    }.addOnFailureListener { e ->
        // this will not work as intended:
        // throw e
    }
}

En effet, le rappel se produit bien longtemps après la fin de l'exécution de la fonction environnante, et le résultat ne peut être renvoyé nulle part. C'est là que suspendCancellableCoroutine entre en jeu, avec l'ajout de continuation à notre bloc de code. Nous pouvons l'utiliser pour renvoyer un résultat à la fonction suspendue ultérieurement, à l'aide de continuation.resume. Traitez le cas où une erreur est générée en utilisant continuation.resumeWithException(e) pour propager correctement l'exception jusqu'au site d'appel.

En général, vous devez toujours vous assurer que vous finirez par obtenir soit un résultat, soit une exception à un moment donné, de façon à ne pas suspendre indéfiniment la coroutine en attendant un résultat.

suspend fun FusedLocationProviderClient.awaitLastLocation(): Location =
   suspendCancellableCoroutine<Location> { continuation ->
       lastLocation.addOnSuccessListener { location ->
           continuation.resume(location)
       }.addOnFailureListener { e ->
           continuation.resumeWithException(e)
       }
   }

Voilà, c'est tout ! Nous venons de présenter une version avec suspension de l'API de dernière position connue qui peut être utilisée par les coroutines de notre application.

Appeler une fonction de suspension

Modifions à présent la fonction getLastKnownLocation dans MainActivity pour appeler la nouvelle version avec coroutine de l'appel de dernière position connue :

private suspend fun getLastKnownLocation() {
    try {
        val lastLocation = fusedLocationClient.awaitLastLocation()
        showLocation(R.id.textView, lastLocation)
    } catch (e: Exception) {
        findAndSetText(R.id.textView, "Unable to get location.")
        Log.d(TAG, "Unable to get location", e)
    }
}

Comme indiqué précédemment, les fonctions de suspension doivent toujours être appelées à partir d'autres fonctions de suspension pour garantir leur exécution dans une coroutine. Cela signifie que nous devons ajouter un modificateur de suspension à la fonction getLastKnownLocation proprement dite, sans quoi une erreur sera générée par l'IDE.

Notez que nous pouvons utiliser un bloc try-catch standard pour le traitement des exceptions. Nous pouvons sortir ce code du rappel en cas d'échec, car les exceptions provenant de l'API Location sont désormais propagées correctement, comme dans un programme impératif standard.

Pour démarrer une coroutine, on utilise généralement CoroutineScope.launch, qui nécessite de définir la portée de la coroutine. Heureusement, les bibliothèques Android KTX proposent plusieurs portées prédéfinies pour les objets de cycle de vie courants comme Activity, Fragment et ViewModel.

Ajoutez le code suivant à Activity.onStart :

override fun onStart() {
   super.onStart()
   if (!hasPermission(ACCESS_FINE_LOCATION)) {
       requestPermissions(arrayOf(ACCESS_FINE_LOCATION), 0)
   }

   lifecycleScope.launch {
       try {
           getLastKnownLocation()
       } catch (e: Exception) {
           findAndSetText(R.id.textView, "Unable to get location.")
           Log.d(TAG, "Unable to get location", e)
       }
   }
   startUpdatingLocation()
}

Vous devez être en mesure d'exécuter votre application et de vérifier qu'elle fonctionne avant de passer à l'étape suivante, qui va consister à intégrer Flow pour une fonction qui renvoie des résultats de localisation plusieurs fois.

Intéressons-nous maintenant à la fonction startUpdatingLocation(). Dans le code actuel, nous allons enregistrer un écouteur auprès de Fused Location Provider pour obtenir des mises à jour périodiques de la position de l'appareil de l'utilisateur chaque fois qu'elle change dans le monde réel.

Afin d'illustrer l'objectif que nous souhaitons atteindre avec une API basée sur Flow, examinons d'abord les éléments de MainActivity que nous allons retirer de cette section pour les insérer dans les détails d'implémentation de notre nouvelle fonction d'extension.

Le code actuel contient une variable qui indique si l'écoute des mises à jour a commencé :

var listeningToUpdates = false

Il contient également une sous-classe de la classe de rappel de base, ainsi que l'intégration de la fonction de rappel en cas de mise à jour de la position :

private val locationCallback: LocationCallback = object : LocationCallback() {
   override fun onLocationResult(locationResult: LocationResult?) {
       if (locationResult != null) {
           showLocation(R.id.textView, locationResult.lastLocation)
       }
   }
}

Nous disposons également de l'enregistrement initial de l'écouteur (qui peut échouer si l'utilisateur n'a pas accordé les autorisations nécessaires), ainsi que de rappels, puisqu'il s'agit d'un appel asynchrone :

private fun startUpdatingLocation() {
   fusedLocationClient.requestLocationUpdates(
       createLocationRequest(),
       locationCallback,
       Looper.getMainLooper()
   ).addOnSuccessListener { listeningToUpdates = true }
   .addOnFailureListener { e ->
       findAndSetText(R.id.textView, "Unable to get location.")
       e.printStackTrace()
   }
}

Enfin, nous procédons à un nettoyage lorsque l'écran n'est plus actif :

override fun onStop() {
   super.onStop()
   if (listeningToUpdates) {
       stopUpdatingLocation()
   }
}

private fun stopUpdatingLocation() {
   fusedLocationClient.removeLocationUpdates(locationCallback)
}

Vous pouvez supprimer tous ces extraits de code de MainActivity en laissant simplement une fonction startUpdatingLocation() vide que nous utiliserons plus tard pour démarrer la collecte Flow.

callbackFlow : constructeur Flow pour les API basées sur le rappel

Ouvrez à nouveau LocationUtils.kt et définissez une autre fonction d'extension sur FusedLocationProviderClient :

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    TODO("Register a location listener")
    TODO("Emit updates on location changes")
    TODO("Clean up listener when finished")
}

Nous devons effectuer quelques opérations pour répliquer les fonctionnalités que nous venons de supprimer du code MainActivity. Nous allons utiliser callbackFlow(), une fonction de constructeur qui renvoie un Flow, ce qui convient à l'émission de données à partir d'une API basée sur le rappel.

Le bloc transmis à callbackFlow() est défini avec ProducerScope comme récepteur.

noinline block: suspend ProducerScope<T>.() -> Unit

ProducerScope encapsule les détails d'implémentation de callbackFlow, par exemple le fait qu'un Channel soutient le Flow créé. Pour résumer, les Channels sont utilisés en interne par certains opérateurs et constructeurs Flow. Si vous ne créez pas votre propre constructeur/opérateur, vous n'avez pas besoin de vous préoccuper de ces détails de bas niveau.

Nous allons simplement utiliser quelques fonctions que ProducerScope expose pour émettre des données et gérer l'état du Flow.

Commençons par créer un écouteur pour l'API de localisation :

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            for (location in result.locations) {
                offer(location) // emit location into the Flow using ProducerScope.offer
            }
        }
    }

    TODO("Register a location listener")
    TODO("Clean up listener when finished")
}

Nous utiliserons ProducerScope.offer pour envoyer les données de localisation au Flow à mesure qu'elles sont disponibles.

Enregistrez ensuite le rappel auprès de FusedLocationProviderClient, en prenant soin de traiter les éventuelles erreurs :

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            for (location in result.locations) {
                offer(location) // emit location into the Flow using ProducerScope.offer
            }
        }
    }

    requestLocationUpdates(
       createLocationRequest(),
       callback,
       Looper.getMainLooper()
    ).addOnFailureListener { e ->
       close(e) // in case of error, close the Flow
    }

    TODO("Clean up listener when finished")
}

FusedLocationProviderClient.requestLocationUpdates est une fonction asynchrone (tout comme lastLocation) qui utilise des rappels pour signaler si elle a abouti à une réussite ou à un échec.

Nous pouvons ignorer l'état de réussite, car il signifie simplement qu'à un moment ultérieur, onLocationResult sera appelé et nous commencerons à transmettre des résultats dans le Flow.

En cas d'échec, nous fermons immédiatement le Flow avec une Exception.

La dernière chose à appeler dans un bloc transmis à callbackFlow est toujours awaitClose. Cette méthode est très utile pour insérer un code de nettoyage afin de libérer des ressources en cas d'achèvement ou d'annulation du Flow (qu'une Exception se soit produite ou non) :

fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            for (location in result.locations) {
                offer(location) // emit location into the Flow using ProducerScope.offer
            }
        }
    }

    requestLocationUpdates(
       createLocationRequest(),
       callback,
       Looper.getMainLooper()
    ).addOnFailureListener { e ->
       close(e) // in case of exception, close the Flow
    }

    awaitClose {
       removeLocationUpdates(callback) // clean up when Flow collection ends
    }
}

Maintenant que nous disposons de tous les éléments nécessaires (enregistrement d'un écouteur, écoute des mises à jour et nettoyage), revenons à MainActivity pour utiliser effectivement Flow afin d'afficher la position.

Collecter le Flow

Modifions à présent la fonction startUpdatingLocation dans MainActivity pour appeler le constructeur Flow et commencer la collecte : Voici à quoi ressemblerait une implémentation simpliste de cette fonction :

private fun startUpdatingLocation() {
    lifecycleScope.launch {
        fusedLocationClient.locationFlow()
        .conflate()
        .catch { e ->
            findAndSetText(R.id.textView, "Unable to get location.")
            Log.d(TAG, "Unable to get location", e)
        }
        .collect { location ->
            showLocation(R.id.textView, location)
            Log.d(TAG, location.toString())
        }
    }
}

Flow.collect() est un opérateur terminal qui déclenche le fonctionnement effectif du Flow. C'est là que nous recevrons toutes les mises à jour de position émises par notre constructeur callbackFlow. Comme collect est une fonction de suspension, elle doit s'exécuter dans une coroutine que nous lançons à l'aide de lifecycleScope.

Vous remarquerez également les opérateurs intermédiaires conflate() et catch() appelés dans le Flow. De nombreux opérateurs sont fournis avec la bibliothèque de coroutines pour vous permettre de filtrer et de transformer les flux de manière déclarative.

L'opérateur conflate signifie que lorsque les mises à jour sont émises plus rapidement que le collecteur ne peut les traiter, nous ne voulons recevoir que la mise à jour la plus récente. Il convient très bien à notre exemple, car nous souhaitons seulement afficher la dernière position dans l'interface utilisateur.

catch vous permet de gérer les éventuelles exceptions générées en amont, en l'occurrence dans le constructeur locationFlow. Une opération en amont est une opération appliquée avant l'opération actuelle.

Quel est le problème dans l'extrait ci-dessus ? Bien qu'il n'entraîne pas le plantage de l'application et qu'il procède correctement à un nettoyage une fois que l'activité est à l'état DESTROYED (grâce à lifecycleScope), il ne tient pas compte des moments où l'activité est arrêtée (par exemple, lorsqu'elle n'est pas visible).

Cela signifie non seulement que nous mettons à jour l'interface utilisateur à des moments où cela n'est pas nécessaire, mais aussi que le Flow maintient l'abonnement aux données de localisation actif, gaspillant ainsi la batterie et les cycles d'utilisation du processeur.

Pour résoudre ce problème, vous pouvez convertir le Flow en LiveData à l'aide de l'extension Flow.asLiveData de la bibliothèque LiveData KTX. LiveData sait à quels moments observer et suspendre l'abonnement. Il redémarre alors le Flow sous-jacent si nécessaire.

private fun startUpdatingLocation() {
    fusedLocationClient.locationFlow()
        .conflate()
        .catch { e ->
            findAndSetText(R.id.textView, "Unable to get location.")
            Log.d(TAG, "Unable to get location", e)
        }
        .asLiveData()
        .observe(this, Observer { location ->
            showLocation(R.id.textView, location)
            Log.d(TAG, location.toString())
        })
}

Le lifecycleScope.launch explicite n'est plus nécessaire, car asLiveData fournit la portée requise pour exécuter le Flow. L'appel observe provient en fait de LiveData et n'a rien à voir avec les coroutines ni Flow. Il s'agit simplement de la méthode standard d'observation de LiveData avec un LifecycleOwner. LiveData collecte le Flow sous-jacent et envoie les positions à son observateur.

La recréation et la collecte des flux sont désormais gérées automatiquement. Nous devons donc déplacer notre méthode startUpdatingLocation() de Activity.onStart (qui peut s'exécuter plusieurs fois) vers Activity.onCreate :

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
   startUpdatingLocation()
}

Vous pouvez désormais exécuter votre application et vérifier sa réaction en cas de rotation, en appuyant sur les boutons "Home" (Accueil) et "Back" (Retour). Consultez le fichier logcat pour vérifier si de nouvelles positions y sont répertoriées lorsque l'application s'exécute en arrière-plan. Si l'implémentation a été correctement effectuée, l'appli doit mettre en pause et redémarrer correctement la collecte Flow lorsque vous appuyez sur "Home" (Accueil), puis que vous revenez à l'application.

Vous venez de créer votre première bibliothèque KTX

Félicitations ! Ce que vous avez fait pendant cet atelier de programmation est très semblable à la procédure habituelle de création d'une bibliothèque d'extensions Kotlin pour une API existante basée sur Java.

Récapitulons ce que vous avez appris :

  • Vous avez ajouté une fonction de commodité pour vérifier les autorisations à partir d'un objet Activity.
  • Vous avez fourni une extension de mise en forme de texte pour l'objet Location.
  • Vous avez utilisé une version avec coroutine des API Location afin d'obtenir la dernière position connue, ainsi que des mises à jour périodiques de la position à l'aide de Flow.
  • Si vous le souhaitez, vous pouvez nettoyer encore le code, ajouter des tests et distribuer votre bibliothèque location-ktx à d'autres développeurs de votre équipe afin qu'ils puissent en profiter.

Pour créer un fichier AAR à distribuer, exécutez la tâche :myktxlibrary:bundleReleaseAar.

Vous pouvez suivre la même procédure pour toute autre API qui pourrait bénéficier des extensions Kotlin.

Affiner l'architecture de l'application avec des Flows

Nous avons déjà indiqué que le lancement d'opérations à partir de l'objet Activity, comme nous l'avons fait dans cet atelier de programmation, n'est pas toujours la solution la plus judicieuse. Vous pouvez suivre cet atelier de programmation pour savoir comment observer les flux de ViewModels dans votre interface utilisateur, comment les flux peuvent interagir avec LiveData et comment concevoir votre application de sorte qu'elle utilise des flux de données.