• 12 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 26/09/2024

Écrivez la couche interface utilisateur de l’architecture MVVM

La couche de données, via les dépôts, la base de données et les entités, est en place ! Vous pouvez maintenant vous attaquer à la couche interface utilisateur.

Ajoutez un conteneur d’état (ViewModel)

Cette couche contient en réalité deux éléments :

  • Le conteneur d'état qui contient des données, qui gère la logique métier et qui expose les données à l’interface graphique. C’est le ViewModel.

  • L’interface graphique qui affiche les données à l'écran à l’aide de XML ou Jetpack Compose. C’est la View.

La couche interface utilisateur est composée de conteneur d'états et de l'interface graphique.
Architecture de l’interface utilisateur

Dans le cadre de l’application PETiSoin, vous allez devoir créer un premier ViewModel, associé au premier écran de l’application qui permet de lister et de récupérer l’ensemble des animaux présents dans la base de données. Pour ce faire, vous devez suivre ces étapes.

Créez votre premier ViewModel

  1. Créez une classeMainActivityViewModel, qui hérite de la classeViewModel, dans un package ui.home.

  2. Passez-lui au sein de son constructeur le dépôtAnimalsRepository.

Si vous utilisez Java : 

Si vous utilisez Kotlin : 

public final class MainActivityViewModel
    extends ViewModel
{
  private final AnimalsRepository animalsRepository;
  public AnimalsViewModel(AnimalsRepository animalsRepository)
  {
    this.animalsRepository = animalsRepository;
  }
}
class MainActivityViewModel(animalsRepository: AnimalsRepository) : ViewModel() {
}

Ajoutez des annotations

Dès ce stade, il est possible de décorer le code à l’aide de deux annotations.

  1. La première est l’annotation@Inject, au niveau du constructeur, pour indiquer à Hilt qu’il est chargé d’injecter le dépôt dans le ViewModel.

  2. La seconde annotation est@HiltViewModel, sur la classeMainActivityViewModelqui permet à Hilt de reconnaître la classe ViewModel pour lui fournir automatiquement les dépendances nécessaires.

Si vous utilisez Java : 

Si vous utilisez Kotlin : 

@HiltViewModel
public final class MainActivityViewModel
    extends ViewModel
{
  private final AnimalsRepository animalsRepository;
  @Inject
  public MainActivityViewModel(AnimalsRepositoryJava animalsRepository)
  {
    this.animalsRepository = animalsRepository;
  }
}
@HiltViewModel
class MainActivityViewModel @Inject constructor(animalsRepository: AnimalsRepository) : ViewModel() {
}

Récupérez l’ensemble des données en Java

Dans le cadre de la version Java, vous pouvez créer une méthodegetAllAnimalsqui retourne simplement le résultat de la méthodegetAllAnimalsde la classeAnimalsRepository.

@HiltViewModel
public final class MainActivityViewModel
    extends ViewModel
{
  //...
  public LiveData<List<Animal>> getAnimals()
  {
    return animalsRepository.getAllAnimals();
  }
}

Récupérez l’ensemble des données en Kotlin

En Kotlin, il est possible de créer un attributanimalqui est unStateFlowet qui renvoie le résultat de la méthodegetAllAnimalsde la classeAnimalsRepository. Cependant, puisque cette méthode renvoie un  Flow, vous devez utiliser la méthode  stateIn   pour convertir leFlowenStateFlow. Cette méthode attend trois paramètres :

  • la portée de la coroutine : vous pouvez utiliser l’attributviewModelScopedisponible dans leViewModel ;

  • la valeur initiale : vous pouvez renseigner une liste vide ;

  • la stratégie de démarrage et d’arrêt du partage du Flow : vous pouvez utiliser SharingStarted.WhileSubscribed(5000).

Euh… C’est quoi ce morceau de code ?

La fonctionWhileSubscribedindique que leFlow ne démarre l'émission de valeurs que lorsqu'il y a au moins une souscription. Lorsqu’il n’y a plus de souscriptions, l’émission est alors arrêtée après un certain délai. Dans le cas du code proposé, le délai est de 5000 millisecondes soit 5 secondes. Ces 5 secondes, qui s’apparentent à un nombre magique, permettent notamment au flux de survivre à un changement de configuration.

@HiltViewModel
class MainActivityViewModel @Inject constructor(animalsRepository: AnimalsRepository) : ViewModel() {
  val animals = animalsRepository.getAllAnimals().stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}

Affichez les données de Room dans une View

Maintenant que le ViewModel est en place, continuons d’implémenter la couche interface utilisateur en ajoutant le second élément, à savoir l’interface graphique (ou la View). Elle va nous permettre d’afficher à l’écran les données que nous avons stockées dans la base de données Room.

Créez le layout XML d’une liste

Comme vous le savez, le but de l’écran est d’afficher l’ensemble des animaux au sein d’une liste et donc d’uneRecyclerView. Pour ce faire, suivez ces étapes. 

  1. Ouvrez le fichier "activity_main.xml" du projet. 

  2.  Modifiez son contenu pour répondre à notre objectif.

<androidx.recyclerview.widget.RecyclerView
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/recyclerView"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
  tools:context=".MainActivity"
  />

Créez le layout XML d’une cellule

Une liste est composée de cellules. La prochaine étape consiste donc à écrire le layout d’une cellule. Dans le cas de notre application PETiSoin, une cellule doit afficher les informations suivantes liées à chaque animal :

  • L’identifiant ; 

  • Le type ; 

  • La race.

<?xml version="1.0" encoding="utf-8"?>
<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="match_parent"
  android:orientation="vertical"
  android:padding="8dp"
  >

  <TextView
    android:id="@+id/id"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:text="@tools:sample/first_names"
    />

  <TextView
    android:id="@+id/type"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:text="@tools:sample/first_names"
    />

  <TextView
    android:id="@+id/name"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:text="@tools:sample/first_names"
    />
</LinearLayout>

Créez l’adaptateur

Puisqu’unAdapterdoit obligatoirement être associé à uneRecyclerView, vous devez créer une classeAnimalsAdapterdans le package ui.home.

Si vous utilisez Java : 

Si vous utilisez Kotlin : 

public final class AnimalsAdapter
    extends Adapter<AnimalViewHolder>
{
  public static final class AnimalViewHolder
      extends ViewHolder
  {
    private final TextView id;
    private final TextView type;
    private final TextView name;
    public AnimalViewHolder(@NonNull ItemAnimalBinding binding)
    {
      super(binding.getRoot());

      this.id = binding.id;
      this.type = binding.type;
      this.name = binding.name;
    }
    public void bind(Animal animal)
    {
      id.setText(String.valueOf(animal.id));
      type.setText(animal.type.name());
      name.setText(animal.name);
    }
  }

  @NonNull
  private List<Animal> animals;
  public AnimalsAdapter(@NonNull List<Animal> animals)
  {
    this.animals = animals;
  }
  @NonNull
  @Override
  public AnimalViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)
  {
    final ItemAnimalBinding binding = ItemAnimalBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
    return new AnimalViewHolder(binding);
  }
  @Override
  public void onBindViewHolder(@NonNull AnimalViewHolder holder, int position)
  {
    holder.bind(animals.get(position));
  }
  @Override
  public int getItemCount()
  {
    return animals.size();
  }
  public void update(List<Animal> animals)
  {
    this.animals = animals;
    notifyDataSetChanged();
  }
}

 

class AnimalsAdapter(private var animals: List<Animal>) :
  RecyclerView.Adapter<AnimalsAdapter.AnimalViewHolder>()
{
  inner class AnimalViewHolder(binding: ItemAnimalBinding) :
    RecyclerView.ViewHolder(binding.root)
  {
    private val id: TextView = binding.id
    private val type: TextView = binding.type
    private val name: TextView = binding.name
    fun bind(animal: Animal)
    {
      id.text = animal.id.toString()
      type.text = animal.type.name
      name.text = animal.name
    }
  }
  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimalViewHolder
  {
    val binding = ItemAnimalBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    return AnimalViewHolder(binding)
  }

  override fun onBindViewHolder(holder: AnimalViewHolder, position: Int)
  {
    holder.bind(animals[position])
  }
  override fun getItemCount(): Int =
    animals.size
  fun update(animals: List<Animal>)
  {
    this.animals = animals
    notifyDataSetChanged()
  }
}

Complétez l’activité

Tout est maintenant prêt pour compléter la classeMainActivityavec le code écrit jusqu’à maintenant. Avant de commencer, vous devriez avoir une classe qui ressemble à ça.

Si vous utilisez Java : 

Si vous utilisez Kotlin : 

public class MainActivity
    extends AppCompatActivity
{
  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    EdgeToEdge.enable(this);
    setContentView(R.layout.activity_main);
    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.recyclerView), (v, insets) -> {
      Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
      v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
      return insets;
    });
  }
}

 

class MainActivity : AppCompatActivity()
{
  override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)
    enableEdgeToEdge()
    setContentView(R.layout.activity_main)
    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.recyclerView)) { v, insets ->
      val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
      v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
      insets
    }
  }
}

1. La première étape est d’injecter le ViewModel dans l’activité grâce à Hilt. Pour faciliter l’injection des dépendances dans les classes Android, vous devez aider Hilt à identifier l’activité comme un composant Android en ajoutant l’annotation@AndroidEntryPointsur la classe.

2. Obtenez une instance de ViewModel.

En Java, vous devez créer une instance de manière classique à l'aide de la classe  ViewModelProvider. En Kotlin, il est possible d’utiliser l’extensionby viewModels.

Si vous utilisez Java : 

Si vous utilisez Kotlin : 

@AndroidEntryPoint
public class MainActivity
    extends AppCompatActivity
{
  private MainActivityViewModel viewModel;
  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);

    EdgeToEdge.enable(this);

    setContentView(R.layout.activity_main);

    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.recyclerView), (v, insets) -> {
      Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
      v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
      return insets;
    });

    viewModel = new ViewModelProvider(this).get(MainActivityViewModel.class);
  }
}

 

@AndroidEntryPoint
class MainActivity : AppCompatActivity()
{
  private val viewModel: MainActivityViewModel by viewModels()
  //…
}

3. L’étape suivante est d’ajouter une instance de la classeAnimalsAdapteren tant qu’attribut de l’activité et de l’attacher à laRecyclerViewpour afficher les données.

Si vous utilisez Java : 

Si vous utilisez Kotlin :

@AndroidEntryPoint
public class MainActivity
    extends AppCompatActivity
{
  private MainActivityViewModel viewModel;
  private final AnimalsAdapter adapter = new AnimalsAdapter(new ArrayList<>());
  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);

    EdgeToEdge.enable(this);

    setContentView(R.layout.activity_main);

    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.recyclerView), (v, insets) -> {
      Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
      v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
      return insets;
    });

    viewModel = new ViewModelProvider(this).get(MainActivityViewModel.class);

    ((RecyclerView) findViewById(R.id.recyclerView)).setAdapter(adapter);
  }
}

 

@AndroidEntryPoint
class MainActivity : AppCompatActivity()
{
  private val viewModel: MainActivityViewModel by viewModels()

  private val adapter = AnimalsAdapter(emptyList())

  override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)

    enableEdgeToEdge()

    setContentView(R.layout.activity_main)

    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.recyclerView)) { v, insets ->
      val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
      v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
      insets
    }
    findViewById<RecyclerView>(R.id.recyclerView).adapter = adapter
  }
}

 4. La dernière étape consiste à observer en temps réel les animaux, que ça soit via les  LiveDataen Java ou via lesFlowen Kotlin, afin de détecter les changements et mettre à jour laRecyclerView .

Si vous utilisez Java : 

Si vous utilisez Kotlin : 

@AndroidEntryPoint
public class MainActivity
    extends AppCompatActivity
{
  private MainActivityViewModel viewModel;
  private final AnimalsAdapter adapter = new AnimalsAdapter(new ArrayList<>());
  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);

    EdgeToEdge.enable(this);

    setContentView(R.layout.activity_main);

    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.recyclerView), (v, insets) -> {
      Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
      v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
      return insets;
    });

    viewModel = new ViewModelProvider(this).get(MainActivityViewModel.class);

    ((RecyclerView) findViewById(R.id.recyclerView)).setAdapter(adapter);

    observeAnimals();
  }

  private void observeAnimals()
  {
    viewModel.getAnimals().observe(this, adapter::update);
  }
}

 

@AndroidEntryPoint
class MainActivity : AppCompatActivity()
{
  private val viewModel: MainActivityViewModel by viewModels()

  private val adapter = AnimalsAdapter(emptyList())

  override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)

    enableEdgeToEdge()

    setContentView(R.layout.activity_main)

    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.recyclerView)) { v, insets ->
      val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
      v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
      insets
    }

    findViewById<RecyclerView>(R.id.recyclerView).adapter = adapter

    observeAnimals()
  }

  private fun observeAnimals()
  {
    lifecycleScope.launch {
      viewModel.animals.collect {
        adapter.update(it)
      }
    }
  }
}

Voici une vidéo qui récapitule les principales étapes pour concevoir la couche interface utilisateur.

À vous de jouer

Contexte

Il est temps de créer votre premier écran de l’application PETiSoin qui respecte la patron de conception MVVM.

Consignes

Dans le projet, disponible sur GitHub (Java ou Kotlin), vous devez créer un nouvel écran, via une nouvelle activité, qui permet de lister l’ensemble des vaccins d’un animal. Ce nouvel écran est accessible après avoir cliqué sur un animal listé sur l’écran principal de l’application. 

Livrables

Vous pouvez dupliquer le projet GitHub pour modifier le code source du projet et fournir un projet dont l’ensemble des tests passe.

En résumé

  • Le ViewModel joue le rôle de conteneur d’état de la couche d’interface utilisateur.

  • Un ViewModel hérite de la classeViewModelmise à disposition par Google.

  • Le ViewModel délègue la récupération des données à un repository.

  • Le ViewModel peut exposer des flux qui peuvent être observés dans la View.

  • La View permet de restituer graphiquement les données aux utilisateurs de l’application.

Il ne nous reste plus qu’à vous souhaiter bonne chance pour la suite de vos aventures en tant que développeur Android ! 

Nous espérons que ce cours vous aura apporté toutes les bases nécessaires à la conception d’une base de données locale dans une application Android native. Votre objectif aura été, ici, de permettre une expérience 100% hors-ligne à vos utilisateurs. Dans le développement logiciel et tout particulièrement dans le développement mobile, les choses évoluent très vite. Restez alors actif sur votre veille pour rester à la page sur vos connaissances.

Exemple de certificat de réussite
Exemple de certificat de réussite