Cet atelier de programmation fait partie du cours Android Kotlin Fundamentals. Vous tirerez le meilleur parti de ce cours si vous suivez les ateliers de programmation dans l'ordre. Tous les ateliers de programmation du cours sont répertoriés sur la page de destination des ateliers de programmation Android Kotlin Fundamentals.
Présentation
Cet atelier de programmation récapitule comment utiliser ViewModel
et les fragments pour implémenter la navigation. N'oubliez pas que l'objectif est de placer la logique when pour accéder à ViewModel
, mais de définir les chemins d'accès dans les fragments et le fichier de navigation. Pour atteindre cet objectif, vous utilisez des modèles de vue, des fragments, LiveData
et des observateurs.
L'atelier de programmation s'achève par une démonstration intelligente du suivi des états des boutons, avec un minimum de code. Ainsi, chaque bouton est activé et cliquable uniquement lorsque l'utilisateur s'appuie dessus.
Ce que vous devez déjà savoir
Vous devez être au fait:
- Créer une interface utilisateur de base à l'aide d'une activité, de fragments et de vues
- Parcourir des fragments et utiliser
safeArgs
pour transmettre des données entre fragments - Affichez des modèles, et découvrez les usines de modèles, les transformations et
LiveData
ainsi que leurs observateurs. - Créer une base de données
Room
, créer un objet d'accès aux données et définir des entités - Utiliser des coroutines pour des interactions avec une base de données et pour d'autres tâches de longue durée
Points abordés
- Comment mettre à jour un enregistrement existant concernant la qualité du sommeil dans la base de données.
- Utiliser
LiveData
pour suivre l'état des boutons - Afficher un snack-bar en réponse à un événement
Objectifs de l'atelier
- Développez l'application TrackMySleepQuality pour obtenir une évaluation de qualité, ajoutez la note à la base de données et affichez le résultat.
- Utilisez
LiveData
pour déclencher l'affichage d'un snack-bar. - Utilisez
LiveData
pour activer et désactiver les boutons.
Dans cet atelier de programmation, vous allez créer l'enregistrement de qualité pour le sommeil et l'interface utilisateur finalisée de l'application TrackMySleepQuality.
L'application dispose de 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. Toutes les données sur le sommeil de l'utilisateur s'affichent à l'écran. Le bouton Effacer supprime définitivement toutes les données collectées par l'utilisateur.
Le deuxième écran, à droite, permet de sélectionner un niveau de qualité du sommeil. Dans l'application, la note est indiquée sous forme numérique. À des fins de développement, l'application affiche les icônes des visages et leurs équivalents numériques.
Le parcours utilisateur est le suivant:
- L'utilisateur ouvre l'application et voit l'écran de suivi du sommeil.
- L'utilisateur appuie sur le bouton Démarrer. Cette action enregistre l'heure de début et l'affiche. Le bouton Start (Démarrer) est désactivé, tout comme le bouton Stop (Arrêter).
- L'utilisateur appuie sur le bouton Arrêter. L'heure de fin est enregistrée, et l'écran de qualité du sommeil s'affiche.
- L'utilisateur sélectionne une icône de qualité du sommeil. L'écran se ferme et l'écran de suivi affiche le temps et la qualité de votre sommeil. Le bouton Stop (Arrêter) est désactivé, tout comme le bouton Start (Démarrer). L'application est prête pour une autre 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 Clear (Effacer), toutes les données sont effacées sans recours. Aucun message ne s'affiche.
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
Cet atelier de programmation suppose que vous savez mettre en œuvre la navigation à l'aide de fragments et du fichier de navigation. Pour vous économiser, vous bénéficiez d'une grande partie de ce code.
Étape 1: Inspectez le code
- Pour commencer, poursuivez avec le code que vous avez obtenu à la fin du dernier atelier de programmation, ou téléchargez le code de démarrage.
- Dans votre code de démarrage, inspectez
SleepQualityFragment
. Cette classe gonfle la mise en page, obtient l'application et renvoiebinding.root
. - Ouvrez le fichier navigation.xml dans l'éditeur de conception. Vous voyez qu'il existe un chemin de navigation de
SleepTrackerFragment
àSleepQualityFragment
, et de nouveau deSleepQualityFragment
àSleepTrackerFragment
. - Inspectez le code pour le fichier navigation.xml. Recherchez en particulier le
<argument>
nommésleepNightKey
.
Lorsque l'utilisateur passe deSleepTrackerFragment
àSleepQualityFragment,
, l'application transmet unsleepNightKey
àSleepQualityFragment
pour la nuit qui doit être mise à jour.
Étape 2: Ajoutez une fonctionnalité de navigation pour le suivi de la qualité du sommeil
Le graphique de navigation inclut déjà les chemins de la SleepTrackerFragment
à la SleepQualityFragment
, puis inversement. Toutefois, les gestionnaires de clics qui implémentent la navigation d'un fragment à l'autre ne sont pas encore codés. Vous ajoutez ce code dans la ViewModel
.
Dans le gestionnaire de clics, vous définissez un LiveData
qui change lorsque vous souhaitez que l'application accède à une autre destination. Le fragment observe ce LiveData
. Lorsque les données changent, le fragment accède à la destination et indique au modèle de vue que cette opération est terminée, ce qui réinitialise la variable d'état.
- Ouvrez
SleepTrackerViewModel
. Vous devez ajouter une barre de navigation pour que, lorsque l'utilisateur appuie sur le bouton Arrêter, l'application accède àSleepQualityFragment
pour obtenir un niveau de qualité. - Dans
SleepTrackerViewModel
, créez unLiveData
qui change lorsque vous souhaitez que l'application accède àSleepQualityFragment
. Utilisez une encapsulation pour n'exposer qu'une version observable deLiveData
àViewModel
.
Vous pouvez placer ce code au niveau supérieur du corps de la classe.
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()
val navigateToSleepQuality: LiveData<SleepNight>
get() = _navigateToSleepQuality
- Ajoutez une fonction
doneNavigating()
qui réinitialise la variable qui déclenche la navigation.
fun doneNavigating() {
_navigateToSleepQuality.value = null
}
- Dans le gestionnaire de clics du bouton Arrêter,
onStopTracking()
, déclenche la navigation versSleepQualityFragment
. Définissez la variable _navigateToSleepQuality
à la fin de la fonction comme dernière chose du bloclaunch{}
. Notez que cette variable est définie surnight
. Lorsque cette variable a une valeur, l'application accède à la valeurSleepQualityFragment
et transmet la nuit.
_navigateToSleepQuality.value = oldNight
- Le
SleepTrackerFragment
doit observer _navigateToSleepQuality
pour que l'application sache quand naviguer. DansSleepTrackerFragment
, ajoutez un observateur pouronCreateView()
dansnavigateToSleepQuality()
. Notez que l'importation est ambiguë et que vous devez importerandroidx.lifecycle.Observer
.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})
- Dans le bloc observateur, parcourez et transmettez l'ID de la nuit, puis appelez
doneNavigating()
. Si votre importation est ambiguë, importezandroidx.navigation.fragment.findNavController
.
night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
sleepTrackerViewModel.doneNavigating()
}
- Créez et exécutez votre application. Appuyez sur Démarrer, puis sur Arrêter pour accéder à l'écran
SleepQualityFragment
. Pour revenir en arrière, utilisez le bouton "Retour" du système.
Dans cette tâche, vous allez enregistrer la qualité du sommeil et revenir au fragment de l'outil de suivi du sommeil. L'affichage doit se mettre à jour automatiquement pour présenter la nouvelle valeur à l'utilisateur. Vous devez créer un ViewModel
et une ViewModelFactory
, puis mettre à jour le SleepQualityFragment
.
Étape 1: Créer un ViewModel et un ViewModelFactory
- Dans le package
sleepquality
, créez ou ouvrez SleepQualityViewModel.kt. - Créez une classe
SleepQualityViewModel
qui utilise unsleepNightKey
et une base de données comme arguments. Tout comme pour leSleepTrackerViewModel
, vous devez transmettre ledatabase
depuis l'usine. Vous devez également transmettre lessleepNightKey
depuis le menu de navigation.
class SleepQualityViewModel(
private val sleepNightKey: Long = 0L,
val database: SleepDatabaseDao) : ViewModel() {
}
- Dans la classe
SleepQualityViewModel
, définissez une valeurJob
etuiScope
, puis remplacezonCleared()
.
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
- Pour revenir à la
SleepTrackerFragment
à l'aide du même schéma que celui ci-dessus, déclarez_navigateToSleepTracker
. ImplémenteznavigateToSleepTracker
etdoneNavigating()
.
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()
val navigateToSleepTracker: LiveData<Boolean?>
get() = _navigateToSleepTracker
fun doneNavigating() {
_navigateToSleepTracker.value = null
}
- Créez un gestionnaire de clics,
onSetSleepQuality()
, pour toutes les images de qualité du sommeil à utiliser.
Utilisez le même modèle de coroutine que dans l'atelier de programmation précédent:
- Lancez une coroutine dans
uiScope
et passez au coordinateur d'E/S. - Récupérez
tonight
à l'aide desleepNightKey
. - Définissez la qualité du sommeil.
- Mettez à jour la base de données.
- Déclenchez la navigation.
Notez que l'exemple de code ci-dessous fonctionne avec le gestionnaire de clics au lieu d'exclure l'opération de base de données dans le contexte.
fun onSetSleepQuality(quality: Int) {
uiScope.launch {
// IO is a thread pool for running operations that access the disk, such as
// our Room database.
withContext(Dispatchers.IO) {
val tonight = database.get(sleepNightKey) ?: return@withContext
tonight.sleepQuality = quality
database.update(tonight)
}
// Setting this state variable to true will alert the observer and trigger navigation.
_navigateToSleepTracker.value = true
}
}
- Dans le package
sleepquality
, créez ou ouvrezSleepQualityViewModelFactory.kt
, puis ajoutez la classeSleepQualityViewModelFactory
, comme indiqué ci-dessous. Cette classe utilise une version identique au code récurrent que vous avez vu précédemment. Examinez le code avant de continuer.
class SleepQualityViewModelFactory(
private val sleepNightKey: Long,
private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) {
return SleepQualityViewModel(sleepNightKey, dataSource) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Étape 2: Mettez à jour le paramètre SleepQualityFragment
- Ouvrez
SleepQualityFragment.kt
. - Dans
onCreateView()
, après avoir obtenu leapplication
, vous devez obtenir learguments
fourni avec la navigation. Cet argument figure dansSleepQualityFragmentArgs
. Vous devez les extraire du groupe.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
- Ensuite, obtenez
dataSource
.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
- Créez une fabrique en transmettant le
dataSource
et lesleepNightKey
.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
- Obtenez une référence
ViewModel
.
val sleepQualityViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(SleepQualityViewModel::class.java)
- Ajoutez le
ViewModel
à l'objet de liaison. Si une erreur se produit au niveau de l'objet de liaison, ignorez-la pour l'instant.
binding.sleepQualityViewModel = sleepQualityViewModel
- Ajoutez l'observateur. Lorsque vous y êtes invité, importez
androidx.lifecycle.Observer
.
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
if (it == true) { // Observed state is true.
this.findNavController().navigate(
SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
sleepQualityViewModel.doneNavigating()
}
})
Étape 3: Mettez à jour le fichier de mise en page et exécutez l'application
- Ouvrez le fichier de mise en page
fragment_sleep_quality.xml
. Dans le bloc<data>
, ajoutez une variable pourSleepQualityViewModel
.
<data>
<variable
name="sleepQualityViewModel"
type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
</data>
- Pour chacune des six images de qualité du sommeil, ajoutez un gestionnaire de clics comme celui présenté ci-dessous. Associez le niveau de qualité à l'image.
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
- Nettoyez et recréez votre projet. Cela devrait résoudre toutes les erreurs liées à l'objet de liaison. Sinon, videz le cache (File > Invalidate Caches / Restart) (Modifier les valeurs et invalider les caches/Redémarrer) et recréez l'application.
Félicitations ! Vous venez de créer une application de base de données Room
complète à l'aide de coroutines.
Votre application fonctionne désormais très bien. L'utilisateur peut appuyer sur Démarrer et Arrêter autant de fois qu'il le souhaite. Lorsque l'utilisateur appuie sur Arrêter, il peut saisir une qualité de sommeil. Lorsque l'utilisateur appuie sur Effacer, toutes les données sont supprimées silencieusement en arrière-plan. Toutefois, tous les boutons sont toujours activés et cliquables, ce qui n'empêche pas l'application de fonctionner. Toutefois, les utilisateurs peuvent créer des nuits de sommeil incomplètes.
Dans cette dernière tâche, vous allez apprendre à utiliser des cartes de transformation pour gérer la visibilité des boutons afin que les utilisateurs puissent uniquement faire le bon choix. Toutefois, une fois la suppression effectuée, vous pouvez utiliser une méthode similaire pour afficher un message d'accueil.
Étape 1: Mettre à jour les états du bouton
L'idée est de définir l'état du bouton de sorte qu'au départ, seul le bouton Start (Démarrer) soit activé, ce qui signifie qu'il est cliquable.
Une fois que l'utilisateur a appuyé sur Démarrer, le bouton Arrêter devient activé, mais pas Démarrer. Le bouton Effacer n'est activé que lorsqu'il contient des données.
- Ouvrez le fichier de mise en page
fragment_sleep_tracker.xml
. - Ajoutez la propriété
android:enabled
à chaque bouton. La propriétéandroid:enabled
est une valeur booléenne qui indique si le bouton est activé ou non. (Vous pouvez appuyer sur un bouton activé, mais pas sur un bouton désactivé.) Attribuez à la propriété la valeur d'une variable d'état que vous définirez immédiatement.
start_button
:
android:enabled="@{sleepTrackerViewModel.startButtonVisible}"
stop_button
:
android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"
clear_button
:
android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
- Ouvrez
SleepTrackerViewModel
et créez trois variables correspondantes. Attribuez à chaque variable une transformation qui la teste.
- Le bouton Démarrer doit être activé lorsque
tonight
est défini surnull
. - Le bouton Arrêter doit être activé lorsque la valeur de
tonight
est différente denull
. - Le bouton Effacer ne doit être activé que si
nights
, et donc la base de données, contient des nuits de sommeil.
val startButtonVisible = Transformations.map(tonight) {
it == null
}
val stopButtonVisible = Transformations.map(tonight) {
it != null
}
val clearButtonVisible = Transformations.map(nights) {
it?.isNotEmpty()
}
- Exécutez votre application et testez les boutons.
Étape 2: Utilisez un snack-bar pour informer l'utilisateur
Une fois que l'utilisateur a effacé la base de données, affichez une confirmation à l'aide du widget Snackbar
. Un snackbar fournit de brèves informations sur une opération par le biais d'un message en bas de l'écran. Un snack-bar disparaît après un délai avant expiration, après une interaction de l'utilisateur ailleurs sur l'écran ou après que l'utilisateur a fait glisser le snack-bar hors de l'écran.
Afficher le snack-bar est une tâche d'interface utilisateur qui doit se produire dans le fragment. La barre de notification s'affiche dans la ViewModel
. Pour configurer et déclencher un snack-bar lorsque les données sont effacées, vous pouvez utiliser la même technique que pour déclencher la navigation.
- Dans
SleepTrackerViewModel
, créez l'événement encapsulé.
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
- Intégrez ensuite
doneShowingSnackbar()
.
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
- Dans
SleepTrackerFragment
, ajoutez un observateur dansonCreateView()
:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
- Dans le bloc observateur, affichez le snack-bar et réinitialisez immédiatement l'événement.
if (it == true) { // Observed state is true.
Snackbar.make(
activity!!.findViewById(android.R.id.content),
getString(R.string.cleared_message),
Snackbar.LENGTH_SHORT // How long to display the message.
).show()
sleepTrackerViewModel.doneShowingSnackbar()
}
- Dans
SleepTrackerViewModel
, déclenchez l'événement dans la méthodeonClear()
. Pour ce faire, définissez la valeur de l'événement surtrue
dans le bloclaunch
:
_showSnackbarEvent.value = true
- Créez et exécutez votre application.
Projet Android Studio : TrackMySleepQualityFinal
La mise en œuvre du suivi de la qualité du sommeil dans cette appli est comme lire de la musique que vous connaissez bien dans une nouvelle clé. Bien que les détails changent, le modèle sous-jacent de ce que vous avez fait dans les ateliers de programmation précédents dans cette leçon reste le même. La connaissance de ces tendances accélère le codage, car il permet de réutiliser le code d'applications existantes. Voici quelques-uns des schémas utilisés dans ce cours:
- Créez un
ViewModel
et unViewModelFactory
, puis configurez une source de données. - Déclenchez la navigation. Pour séparer les problèmes, placez le gestionnaire de clics dans le modèle d'affichage et le menu de navigation dans le fragment.
- Utilisez l'encapsulation avec
LiveData
pour suivre les changements d'état et y répondre. - Utilisez des transformations avec
LiveData
. - Créez une base de données singleton.
- Définissez des coroutines pour les opérations de base de données.
Déclenchement de la navigation
Vous pouvez définir les chemins de navigation possibles entre les fragments dans un fichier de navigation. Il existe plusieurs manières de déclencher la navigation d'un fragment à l'autre. Exemples :
- Définissez des gestionnaires
onClick
pour déclencher la navigation vers un fragment de destination. - Pour activer la navigation d'un fragment à l'autre, procédez comme suit:
- Définissez une valeur
LiveData
à enregistrer si la navigation doit se produire. - Associer un observateur à cette valeur
LiveData
. - Votre code modifie ensuite cette valeur chaque fois que la navigation doit être déclenchée ou terminée.
Définir l'attribut android:enabled
- L'attribut
android:enabled
est défini dansTextView
et hérité de toutes les sous-classes, y comprisButton
. - L'attribut
android:enabled
détermine si un paramètreView
est activé. La signification de "Activé" varie en fonction des sous-classes. Par exemple, un élémentEditText
non activé empêche l'utilisateur de modifier le texte qu'il contient, et un élémentButton
désactivé empêche l'utilisateur d'appuyer sur le bouton. - L'attribut
enabled
est différent de l'attributvisibility
. - Vous pouvez utiliser des cartes de transformation pour définir la valeur de l'attribut des boutons
enabled
en fonction de l'état d'un autre objet ou d'une autre variable.
Autres points abordés dans cet atelier de programmation:
- Pour déclencher des notifications, vous pouvez utiliser la même technique que pour déclencher la navigation.
- Vous pouvez utiliser un
Snackbar
pour avertir l'utilisateur.
Cours Udacity:
Documentation pour les développeurs Android:
Cette section répertorie les devoirs possibles pour les élèves qui effectuent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. C'est à l'enseignant de procéder comme suit:
- Si nécessaire, rendez-les.
- Communiquez aux élèves comment rendre leurs devoirs.
- Notez les devoirs.
Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ils n'ont pas besoin d'attribuer les devoirs de leur choix.
Si vous suivez vous-même cet atelier de programmation, n'hésitez pas à utiliser ces devoirs pour tester vos connaissances.
Répondez à ces questions.
Question 1
Pour que votre application puisse déclencher la navigation d'un fragment à l'autre, vous pouvez utiliser une valeur LiveData
afin d'indiquer si la navigation doit être déclenchée ou non.
Quelles sont les étapes d'utilisation d'une valeur LiveData
, appelée gotoBlueFragment
, pour déclencher la navigation entre le fragment rouge et le fragment bleu ? Sélectionnez toutes les réponses applicables :
- Dans
ViewModel
, définissez la valeurLiveData
surgotoBlueFragment
. - Dans
RedFragment
, observez la valeurgotoBlueFragment
. Implémentez le codeobserve{}
pour accéder àBlueFragment
le cas échéant, puis réinitialisez la valeur degotoBlueFragment
pour indiquer que la navigation est terminée. - Assurez-vous que votre code définit la variable
gotoBlueFragment
sur la valeur qui déclenche la navigation à chaque fois que l'application doit passer deRedFragment
àBlueFragment
. - Assurez-vous que votre code définit un gestionnaire
onClick
pour l'élémentView
sur lequel l'utilisateur clique pour accéder àBlueFragment
, où le gestionnaireonClick
observe la valeurgoToBlueFragment
.
Question 2
Vous pouvez vérifier si un Button
est activé (cliquable) ou non en utilisant LiveData
. Comment vous assurez-vous que votre application modifie le bouton UpdateNumber
pour que:
- Le bouton est activé si la valeur de
myNumber
est supérieure à 5. - Le bouton n'est pas activé si
myNumber
est égal ou inférieur à 5.
Supposons que la mise en page contenant le bouton UpdateNumber
inclut la variable <data>
pour NumbersViewModel
, comme illustré ci-dessous:
<data> <variable name="NumbersViewModel" type="com.example.android.numbersapp.NumbersViewModel" /> </data>
Supposons que l'ID du bouton figure dans le fichier de mise en page:
android:id="@+id/update_number_button"
Que devez-vous faire d'autre ? Sélectionnez toutes les réponses applicables.
- Dans la classe
NumbersViewModel
, définissez une variableLiveData
,myNumber
, qui représente le nombre. Définissez également une variable dont la valeur est définie en appelantTransform.map()
sur la variablemyNumber
, qui renvoie une valeur booléenne indiquant si le nombre est supérieur ou égal à 5.
Plus précisément, dans le fichierViewModel
, ajoutez le code suivant:
val myNumber: LiveData<Int>
val enableUpdateNumberButton = Transformations.map(myNumber) {
myNumber > 5
}
- Dans la mise en page XML, définissez l'attribut
android:enabled
deupdate_number_button button
surNumberViewModel.enableUpdateNumbersButton
.
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
- Dans la
Fragment
qui utilise la classeNumbersViewModel
, ajoutez un observateur à l'attributenabled
du bouton.
Plus précisément, dans le fichierFragment
, ajoutez le code suivant:
// Observer for the enabled attribute
viewModel.enabled.observe(this, Observer<Boolean> { isEnabled ->
myNumber > 5
})
- Dans le fichier de mise en page, définissez l'attribut
android:enabled
deupdate_number_button button
sur"Observable"
.
Démarrez la leçon suivante :
Pour obtenir des liens vers d'autres ateliers de programmation dans ce cours, consultez la page de destination des ateliers de programmation Android Kotlin Fundamentals.