Gérez l’UI selon l’état réseau
Bonjour chère étoile filante,
La phase tant attendue d'affichage des données est enfin arrivée avec le design d'Adele que tu retrouveras dans ce mail !
L'utilisation du RecyclerView peut sembler un peu délicate, mais rien d’insurmontable. Prends ton temps pour comprendre toutes les parties qui le composent. Nous allons mettre en place les StateFlows à présent, une manière élégante de mettre à jour l’interface.
Prêt à illuminer l'écran ?
Des journées claires, même avec des bugs.
Cordialement,
Margaret Hamilton, Dev Senior – Projet "Planète Exploration"
Explorez le fonctionnement des StateFlows
Les StateFlows
sont particulièrement adaptés pour gérer l'état dynamique de l'interface utilisateur. Dans notre cas, nous utiliserons un StateFlow
(HomeUiState) pour représenter l'état actuel des données météo dans l'interface utilisateur.
Nous allons modifier la classe MainActivity
, automatiquement générée par Android Studio à la création du projet, par ce code :
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import com.openclassrooms.stellarforecast.databinding.ActivityMainBinding
import com.openclassrooms.stellarforecast.domain.model.WeatherReportModel
import com.openclassrooms.stellarforecast.presentation.home.HomeViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: HomeViewModel by viewModels()
//TODO 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
defineRecyclerView()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
updateCurrentWeather(it.forecast)
}
}
}
}
private fun updateCurrentWeather(forecast: List<WeatherReportModel> {
//TODO 2
}
private fun defineRecyclerView(){
//TODO 3
}
}
Ce code concerne la configuration et l'interaction de l'activité principale avec le ViewModel. Voici une explication détaillée :
private val viewModel: HomeViewModel by viewModels()
: Déclare une propriété viewModel qui utilise la fonctionviewModels()
pour obtenir une instance du ViewModel associé à cette activité. L'utilisation deby viewModels()
est une façon recommandée de créer et d'accéder au ViewModel dans une activité.lifecycleScope.launch { ... }
: Utilise le lifecycleScope de l'activité pour lancer une coroutine.repeatOnLifecycle(Lifecycle.State.STARTED) { ... }
: Cette fonction du lifecycleScope assure que le bloc de code à l'intérieur est exécuté tant que le cycle de vie de l'activité est dans l'état STARTED. Cela garantit que les opérations de collecte (écoute) sur leStateFlow
sont actives lorsque l'activité est visible à l'utilisateur.viewModel.uiState.collect { ... }
: Utilise la fonction collect sur le StateFlow uiState du ViewModel pour écouter les mises à jour de l'état de l'interface utilisateur de manière réactive. En théorie, un flow peut émettre plusieurs valeurs à la suite.uiState.forecast?.let { forecast -> ... }
: Vérifie si l'objet forecast dans l'état de l'interface utilisateur n'est pas nul, puis exécute le bloc de code à l'intérieur. Cela permet d'effectuer des actions spécifiques lorsque de nouvelles données météorologiques sont disponibles.
Nous disposons maintenant de nos données de météo dans la plus haute couche de notre application, c'est-à- dire l’interface utilisateur. Il ne nous reste plus qu'à la dessiner ! Nous reviendrons sur les //TODO
dans le chapitre suivant.
Implémentez la RecyclerView
La RecyclerView est un composant Android et flexible conçu pour afficher une liste de données de manière efficace. Son utilisation est recommandée parce que :
Efficace : Le RecyclerView recycle les éléments de vue, minimalisant ainsi la consommation de ressources.
Flexible : Il offre la possibilité de personnaliser la disposition, le comportement et l'apparence des éléments de la liste.
Gère les modifications de données : Le RecyclerView gère de manière efficace les modifications dynamiques de la liste grâce à des fonctionnalités telles que
DiffUtil
.
Un RecyclerView est composé de plusieurs éléments essentiels pour permettre l'affichage efficace d'une liste de données dans une interface utilisateur. Ces composants sont les suivants :
Adapter :
L'Adapter a pour rôle de lier les données à la vue. Il agit comme un pont entre les données, généralement stockées dans une liste, et la vue qui les affiche. L'adapter crée également les ViewHolders nécessaires pour chaque élément de données, ce qui permet un recyclage efficace des vues et une meilleure performance.
ViewHolder :
Un ViewHolder représente chaque élément individuel dans la liste. Il conserve une référence aux vues à l'intérieur de chaque élément de la liste, ce qui évite de rechercher ces vues à chaque mise à jour.
LayoutManager :
Le LayoutManager est responsable de la disposition des éléments dans le RecyclerView. Il détermine comment les éléments sont arrangés, s'ils forment une liste verticale, horizontale ou une grille.
Affichez les éléments de la liste avec ViewHolder
Nous allons implémenter le code pour nous permettre d’afficher cette liste fournie par Margaret :
Ouvrez le fichier déjà présent “main_activity.xml", et remplacez le code par une déclaration de RecyclerView :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/relative_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
tools:listitem="@layout/item_weather"
android:layout_height="match_parent" />
</RelativeLayout>
Ajoutez un layout “item_weather.xml” dans le sous-dossier “layout” du dossier “res”, comme vous l’avez vu lors des précédents cours. Voici le code qu’il faut ajouter :
<!-- item_weather.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
androidrientation="horizontal"
androidadding="16dp">
<TextView
android:id="@+id/textViewDateTime"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="18sp"
android:textStyle="bold"
tools:text="15/01 - 16:00" />
<TextView
android:id="@+id/textViewStargazing"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
tools:text="👍" />
</LinearLayout>
Nous affichons sur une ligne les informations météorologiques. C’est l'élément que le ViewHolder viendra compléter et modifier au besoin. L'option tools:text
ou tools:listitem
est utilisée pour afficher des données fictives afin d'obtenir une représentation visuelle uniquement dans Android Studio.
Liez les données au design avec Adapter
Nous allons créer une nouvelle classe nommée presentation.home.WeatherAdapter
, comme nous l'avons déjà créée précédemment.
L'Adapter sert à lier les données à la RecyclerView et à créer les vues pour chaque élément de la liste. Voici comment vous pouvez définir un Adapter pour votre RecyclerView :
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.openclassrooms.stellarforecast.databinding.ItemWeatherBinding
import com.openclassrooms.stellarforecast.domain.model.WeatherReportModel
import java.text.SimpleDateFormat
import java.util.Locale
class WeatherAdapter() : ListAdapter<WeatherReportModel, WeatherAdapter.WeatherViewHolder>(DiffCallback) {
class WeatherViewHolder(
private val binding: ItemWeatherBinding,
) : RecyclerView.ViewHolder(binding.root) {
private val dateFormatter = SimpleDateFormat("dd/MM - HH:mm", Locale.getDefault())
fun bind(observation: WeatherReportModel) {
val formattedDate: String = dateFormatter.format(observation.date.time)
binding.textViewDateTime.text = formattedDate
binding.textViewStargazing.text = if (observation.isGoodForStargazing) "⭐️" else "☁️"
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeatherViewHolder {
val itemView = ItemWeatherBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return WeatherViewHolder(itemView)
}
override fun onBindViewHolder(holder: WeatherViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
companion object {
private val DiffCallback = object : DiffUtil.ItemCallback<WeatherReportModel>() {
override fun areItemsTheSame(oldItem: WeatherReportModel, newItem: WeatherReportModel): Boolean {
return oldItem.date == newItem.date
}
override fun areContentsTheSame(oldItem: WeatherReportModel, newItem: WeatherReportModel): Boolean {
return oldItem == newItem
}
}
}
}
Voici une explication des fonctions clés de l’Adapter :
WeatherAdapter :
WeatherViewHolder
: Classe interne représentant chaque élément de la liste, responsable de la liaison des données avec les vues.onCreateViewHolder
: Crée et retourne un nouvel objet ViewHolder pour chaque élément de la liste.onBindViewHolder
: Associe l'élément à la vue correspondante dans le ViewHolder.WeatherViewHolder :
bind
: Lie les propriétés des données de prévisions météorologiques aux éléments de la vue, tels que la température, les températures maximale et minimale, ainsi que les précipitations. C’est la méthode que vous modifierez le plus afin d’afficher les données souhaitées.DiffCallback :
areItemsTheSame et areContentsTheSame
: Aide à déterminer quelles données ont changé entre les anciennes et les nouvelles listes d'éléments. Cela aide à optimiser l'affichage en mettant à jour uniquement les parties nécessaires de l'interface utilisateur lorsqu'il y a des changements. Dans notre cas, nous vérifions seulement si la date change.
Liez le State avec les vues ItemView
Pour maintenir une interface utilisateur réactive et à jour, il est crucial de lier le State (état) de votre application avec les vues de la RecyclerView. Revenons dans MainActivity et remplaçons les TODO
.
Remplacer le TODO 1
par :
private val customAdapter = WeatherAdapter()
Remplacer le TODO 2
par :
private fun updateCurrentWeather(forecast: List<WeatherReportModel> {
customAdapter.submitList(forecast)
}
En utilisant la fonction collect, nous observons les changements d'état. À chaque émission d'une nouvelle liste de données, l'ensemble des données de l'Adapter de la RecyclerView est mis à jour, assurant ainsi une représentation en temps réel des informations dans l'interface utilisateur.
Ainsi, cette approche garantit une gestion efficace des changements d'état, une mise à jour réactive de l'interface utilisateur et une expérience utilisateur fluide au sein de votre application Android.
Contrôlez la disposition des éléments avec LayoutManager
Le LayoutManager contrôle la disposition des éléments de la RecyclerView. Vous pouvez choisir parmi plusieurs gestionnaires, tels que LinearLayoutManager
, GridLayoutManager
ou StaggeredGridLayoutManager
, selon vos besoins. Voici comment définir un LinearLayoutManager
:
Mettez à jour les TODO 3
:
private fun defineRecyclerView(){
val layoutManager = LinearLayoutManager(applicationContext)
binding.recyclerView.layoutManager = layoutManager
binding.recyclerView.adapter = customAdapter
}
À vous de jouer
Contexte
En arrivant ce matin, vous vous rendez compte que tout le monde a posé son manteau, serait-ce le début d’un redoux ?
Parfait ! Votre application est presque terminée, c’est le moment d'intégrer les derniers éléments graphiques au sein d’une RecyclerView.
Nous afficherons une liste de données météorologiques, ordonnée par heure, indiquant l’heure et si nous avons l’accord pour faire tourner les canons à neige.
L’accord est régi par une température inférieure à zéro et des précipitations nulles.
On frappe à votre porte, un collègue vous demande :
Est-ce que l’app sera prête pour ce soir ? Car c’est le Grand Soir !
Consignes
Votre mission actuelle consiste à :
Définir une classe Recyclerview et ses dépendances : Adapter, la vue XML des Item, l’ajout du RecyclerView dans l’Activity.
Corrigé
Le corrigé est disponible sur GitHub à cette adresse :
Déclaration du RecyclerView.
Déclaration de l’Adapter.
Mise à jour du XML de l’Activity.
La définition de la vue des Items XML.
En résumé
Un RecyclerView est un composant d'Android utilisé pour afficher une liste de données dynamique dans une interface utilisateur ; il utilise un Adapter pour lier les données à la vue qui correspondent aux ViewHolders, représentant chaque élément de la liste.
L'Adapter gère la création des ViewHolders (
onCreateViewHolder
), l'association des données avec les vues (bind
) et la gestion des mises à jour dynamiques de l'interface utilisateur.Les ViewHolders représentent la partie graphique de la RecyclerView, ce sont les éléments de la liste que nous lions à un fichier XML.
Maintenant que nous avons compris le fonctionnement de base du RecyclerView, plongeons dans les interactions utilisateur, en particulier la gestion des clics sur les éléments de la liste.