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 :
|
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 :
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 :
Appuyez sur le bouton Exécuter pour tester votre application :
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
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 utilitaireActivityCompat
, 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 valeurtrue
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'uneclass
) signifie que nous définissons une fonction de niveau supérieur dans le fichier. Activity.hasPermission
définit une fonction d'extension nomméehasPermission
sur un récepteur de typeActivity
.- Elle utilise l'autorisation en tant qu'argument
String
et renvoie une valeurBoolean
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.
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 :
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 fonctionsuspend
.- 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 deFlow
. - 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.