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
Cet atelier de programmation récapitule comment utiliser ViewModel
et les fragments ensemble pour implémenter la navigation. N'oubliez pas que l'objectif est de placer la logique de when pour la navigation dans 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 ViewModels, des fragments, LiveData
et des observateurs.
L'atelier de programmation se termine par une méthode astucieuse pour suivre l'état des boutons avec un minimum de code, afin que chaque bouton soit activé et cliquable uniquement lorsque l'utilisateur peut appuyer dessus.
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 et de vues.
- Naviguer entre les fragments et utiliser
safeArgs
pour transmettre des données entre les fragments. - Affichez les modèles, les fabriques de modèles, les transformations et les
LiveData
, ainsi que leurs observateurs. - Création d'une base de données
Room
, d'un objet d'accès aux données (DAO) et définition d'entités - Utiliser des coroutines pour les interactions avec la base de données et d'autres tâches de longue durée
Points abordés
- Comment modifier un enregistrement de qualité du sommeil existant dans la base de données.
- Comment utiliser
LiveData
pour suivre les états des boutons. - Comment afficher une snackbar en réponse à un événement.
Objectifs de l'atelier
- Enrichissez l'application TrackMySleepQuality pour qu'elle puisse recevoir des notes d'évaluation, les ajouter à la base de données et afficher 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 la qualité du sommeil et l'UI finalisée 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
Cet atelier de programmation suppose que vous savez comment implémenter la navigation à l'aide de fragments et du fichier de navigation. Pour vous faire gagner du temps, une grande partie de ce code est fournie.
Étape 1 : Inspecter le code
- Pour commencer, continuez avec votre propre code à 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 augmente la mise en page, obtient l'application et renvoiebinding.root
. - Ouvrez navigation.xml dans l'éditeur de conception. Vous voyez qu'il existe un chemin de navigation de
SleepTrackerFragment
àSleepQualityFragment
, et inversement deSleepQualityFragment
àSleepTrackerFragment
. - Inspectez le code de 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 la navigation pour le suivi de la qualité du sommeil
Le graphique de navigation inclut déjà les chemins d'accès de SleepTrackerFragment
à SleepQualityFragment
et inversement. Toutefois, les gestionnaires de clics qui implémentent la navigation d'un fragment à l'autre ne sont pas encore codés. Ajoutez ce code maintenant dans 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 qu'il a terminé, ce qui réinitialise la variable d'état.
- Ouvrez
SleepTrackerViewModel
. Vous devez ajouter une navigation pour que, lorsque l'utilisateur appuie sur le bouton Arrêter, l'application accède àSleepQualityFragment
pour collecter une note de qualité. - Dans
SleepTrackerViewModel
, créez unLiveData
qui change lorsque vous souhaitez que l'application accède àSleepQualityFragment
. Utilisez l'encapsulation pour n'exposer qu'une version récupérable deLiveData
àViewModel
.
Vous pouvez placer ce code n'importe où 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 Stop,
onStopTracking()
, déclenchez la navigation versSleepQualityFragment
. Définissez la variable _navigateToSleepQuality
à la fin de la fonction, en dernier dans le bloclaunch{}
. Notez que cette variable est définie surnight
. Lorsque cette variable a une valeur, l'application accède àSleepQualityFragment
, en transmettant la nuit.
_navigateToSleepQuality.value = oldNight
SleepTrackerFragment
doit observer _navigateToSleepQuality
pour que l'application sache quand naviguer. DansSleepTrackerFragment
, dansonCreateView()
, ajoutez un observateur pournavigateToSleepQuality()
. Notez que l'importation pour cela est ambiguë et que vous devez importerandroidx.lifecycle.Observer
.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})
- Dans le bloc d'observateur, accédez à l'ID de la nuit en cours et transmettez-le, puis appelez
doneNavigating()
. Si votre importation est ambiguë, importezandroidx.navigation.fragment.findNavController
.
night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
sleepTrackerViewModel.doneNavigating()
}
- Compilez et exécutez votre application. Appuyez sur Start (Démarrer), puis sur Stop (Arrêter), ce qui vous redirige vers 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 du suivi du sommeil. L'affichage doit se mettre à jour automatiquement pour afficher la nouvelle valeur à l'utilisateur. Vous devez créer un ViewModel
et un ViewModelFactory
, et mettre à jour le SleepQualityFragment
.
Étape 1 : Créez un ViewModel et une ViewModelFactory
- Dans le package
sleepquality
, créez ou ouvrez SleepQualityViewModel.kt. - Créez une classe
SleepQualityViewModel
qui accepte unsleepNightKey
et une base de données comme arguments. Comme pourSleepTrackerViewModel
, vous devez transmettredatabase
depuis la fabrique. Vous devez également transmettre lesleepNightKey
de la navigation.
class SleepQualityViewModel(
private val sleepNightKey: Long = 0L,
val database: SleepDatabaseDao) : ViewModel() {
}
- Dans la classe
SleepQualityViewModel
, définissezJob
etuiScope
, puis remplacezonCleared()
.
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
- Pour revenir à
SleepTrackerFragment
en utilisant le même modèle que 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 répartiteur d'E/S. - Obtenez
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 effectue tout le travail dans le gestionnaire de clics, au lieu de factoriser l'opération de base de données dans le contexte différent.
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 du même code récurrent que celui 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 SleepQualityFragment
- Ouvrez
SleepQualityFragment.kt
. - Dans
onCreateView()
, après avoir obtenu leapplication
, vous devez obtenir learguments
fourni avec la navigation. Ces arguments se trouvent dansSleepQualityFragmentArgs
. Vous devez les extraire du bundle.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
- Ensuite, récupérez le
dataSource
.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
- Créez une fabrique en transmettant
dataSource
etsleepNightKey
.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
- Obtenez une référence
ViewModel
.
val sleepQualityViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(SleepQualityViewModel::class.java)
- Ajoutez
ViewModel
à l'objet de liaison. (Si une erreur s'affiche avec l'objet de liaison, ignorez-la pour le moment.)
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 ci-dessous. Associez le niveau de qualité à l'image.
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
- Nettoyez et reconstruisez votre projet. Cela devrait résoudre les erreurs liées à l'objet de liaison. Sinon, videz le cache (File > Invalidate Caches / Restart) et reconstruisez votre 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 parfaitement. 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 la qualité de son sommeil. Lorsque l'utilisateur appuie sur Effacer, toutes les données sont effacées en arrière-plan, sans notification. Toutefois, tous les boutons sont toujours activés et cliquables, ce qui ne casse pas l'application, mais permet aux utilisateurs de créer des nuits de sommeil incomplètes.
Dans cette dernière tâche, vous allez apprendre à utiliser les cartes de transformation pour gérer la visibilité des boutons afin que les utilisateurs ne puissent faire que le bon choix. Vous pouvez utiliser une méthode similaire pour afficher un message convivial une fois que toutes les données ont été effacées.
Étape 1 : Mettez à jour les états des boutons
L'idée est de définir l'état du bouton de sorte qu'au début, seul le bouton Start (Démarrer) soit activé, ce qui signifie qu'il est cliquable.
Une fois que l'utilisateur appuie sur Démarrer, le bouton Arrêter est activé, mais pas celui de Démarrer. Le bouton Effacer n'est activé que lorsque la base de données 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 dans un instant.
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 Start (Démarrer) doit être activé lorsque
tonight
estnull
. - Le bouton Arrêter doit être activé lorsque
tonight
n'est pasnull
. - 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 une snackbar pour informer l'utilisateur
Une fois que l'utilisateur a effacé la base de données, affichez-lui une confirmation à l'aide du widget Snackbar
. Une snackbar fournit un bref retour d'information sur une opération via un message en bas de l'écran. Une snackbar disparaît après un délai d'expiration, après une interaction de l'utilisateur ailleurs sur l'écran ou après que l'utilisateur l'a balayée hors de l'écran.
L'affichage de la snackbar est une tâche d'UI qui doit avoir lieu dans le fragment. La décision d'afficher le snackbar est prise dans ViewModel
. Pour configurer et déclencher une snackbar lorsque les données sont effacées, vous pouvez utiliser la même technique que pour déclencher la navigation.
- Dans le
SleepTrackerViewModel
, créez l'événement encapsulé.
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
- Implémentez ensuite
doneShowingSnackbar()
.
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
- Dans
SleepTrackerFragment
, dansonCreateView()
, ajoutez un observateur :
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
- Dans le bloc d'observateur, affichez la snackbar 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
Implémenter le suivi de la qualité du sommeil dans cette application, c'est comme jouer un morceau de musique familier dans une nouvelle tonalité. 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 de cette leçon reste le même. En connaissant ces modèles, vous pouvez coder beaucoup plus rapidement, car vous pouvez réutiliser le code des applications existantes. Voici quelques-uns des modèles utilisés jusqu'à présent dans ce cours :
- Créez un
ViewModel
et unViewModelFactory
, puis configurez une source de données. - Déclenchez la navigation. Pour séparer les préoccupations, placez le gestionnaire de clics dans le modèle de vue et la 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.
- Configurez des coroutines pour les opérations de base de données.
Déclencher la navigation
Vous définissez les chemins de navigation possibles entre les fragments dans un fichier de navigation. Il existe différentes façons de déclencher la navigation d'un fragment à un autre. Exemples :
- Définissez des gestionnaires
onClick
pour déclencher la navigation vers un fragment de destination. - Vous pouvez également activer la navigation d'un fragment à l'autre :
- Définissez une valeur
LiveData
à enregistrer si la navigation doit avoir lieu. - Associez un observateur à cette valeur
LiveData
. - Votre code modifie ensuite cette valeur chaque fois que la navigation doit être déclenchée ou est terminée.
Définir l'attribut android:enabled
- L'attribut
android:enabled
est défini dansTextView
et hérité par toutes les sous-classes, y comprisButton
. - L'attribut
android:enabled
détermine si unView
est activé ou non. La signification de "activé" varie selon la sous-classe. Par exemple, unEditText
non activé empêche l'utilisateur de modifier le texte qu'il contient, et unButton
non activé 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
enabled
des boutons 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 à l'utilisateur, 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 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épondre aux questions suivantes
Question 1
Pour permettre à votre application de déclencher la navigation d'un fragment à l'autre, vous pouvez utiliser une valeur LiveData
pour indiquer si la navigation doit être déclenchée ou non.
Quelles sont les étapes à suivre pour utiliser une valeur LiveData
, appelée gotoBlueFragment
, afin de déclencher la navigation du fragment rouge vers le fragment bleu ? Sélectionnez toutes les réponses appropriées :
- 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 leView
sur lequel l'utilisateur clique pour accéder àBlueFragment
, où le gestionnaireonClick
observe la valeurgoToBlueFragment
.
Question2
Vous pouvez modifier l'état d'activation (cliquable ou non) d'un Button
à l'aide de LiveData
. Comment vous assureriez-vous que votre application modifie le bouton UpdateNumber
de sorte que :
- Le bouton est activé si
myNumber
a une valeur supérieure à 5. - Le bouton n'est pas activé si
myNumber
est inférieur ou égal à 5.
Supposons que la mise en page contenant le bouton UpdateNumber
inclut la variable <data>
pour NumbersViewModel
, comme indiqué ici :
<data> <variable name="NumbersViewModel" type="com.example.android.numbersapp.NumbersViewModel" /> </data>
Supposons que l'ID du bouton dans le fichier de mise en page soit le suivant :
android:id="@+id/update_number_button"
Que devez-vous faire d'autre ? Plusieurs réponses possibles.
- 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 à 5 ou non.
Plus précisément, dansViewModel
, 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 le
Fragment
qui utilise la classeNumbersViewModel
, ajoutez un observateur à l'attributenabled
du bouton.
Plus précisément, dansFragment
, 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"
.
Passez à la leçon suivante :
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.