Lire et mettre à jour des données avec Room

1. Avant de commencer

Dans les précédents ateliers de programmation, vous avez appris à utiliser une bibliothèque de persistance Room, une couche d'abstraction, mais aussi une base de données SQLite pour stocker les données de l'application. Dans cet atelier, vous allez ajouter des fonctionnalités à l'application Inventory et découvrir comment lire, afficher, mettre à jour et supprimer des données de la base de données SQLite à l'aide de Room. Vous allez utiliser une LazyColumn pour afficher les données de la base de données et les mettre automatiquement à jour lorsque les données sous-jacentes de la base de données seront modifiées.

Conditions préalables

  • Vous savez comment créer une base de données SQLite et comment interagir avec elle à l'aide de la bibliothèque Room.
  • Vous savez comment créer une entité, un objet d'accès aux données (DAO, Data Access Object) et des classes de base de données.
  • Vous savez utiliser un DAO pour mapper des fonctions Kotlin à des requêtes SQL.
  • Vous savez afficher les éléments d'une liste dans une LazyColumn.
  • Vous avez suivi l'atelier de programmation précédent de ce module, intitulé Persistance des données avec Room.

Points abordés

  • Lire et afficher des entités à partir d'une base de données SQLite.
  • Mettre à jour et supprimer des entités d'une base de données SQLite à l'aide de la bibliothèque Room.

Objectifs de l'atelier

  • Créer une application Inventory qui affiche une liste d'articles d'inventaire et qui permet de mettre à jour, de modifier et de supprimer des éléments de la base de données de l'application à l'aide de Room

Ce dont vous avez besoin

  • Un ordinateur avec Android Studio

2. Présentation de l'application de démarrage

Cet atelier de programmation utilise le code de solution de l'application Inventory de l'atelier de programmation précédent, intitulé Persistance des données avec Room, comme code de démarrage. L'application de démarrage enregistre déjà les données à l'aide de la bibliothèque de persistance Room. L'écran Add Item (Ajouter un article) permet à l'utilisateur d'ajouter des données à la base de données de l'application.

Écran "Add Item" (Ajouter un article) avec les champs de texte vides

Sur un téléphone, l'application affiche un inventaire vide

Dans cet atelier de programmation, vous allez étendre l'application pour lire et afficher les données, et mettre à jour et supprimer des entités dans la base de données à l'aide d'une bibliothèque Room.

Télécharger le code de démarrage pour cet atelier de programmation

Pour commencer, téléchargez le code de démarrage :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git
$ cd basic-android-kotlin-compose-training-inventory-app
$ git checkout room

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.

Si vous le souhaitez, vous pouvez consulter le code de démarrage de cet atelier de programmation sur GitHub.

3. Mettre à jour l'état de l'UI

Dans cette tâche, vous allez ajouter une LazyColumn à l'application pour afficher les données stockées dans la base de données.

Écran de téléphone avec des articles d'inventaire

Tutoriel de la fonction composable HomeScreen

  • Ouvrez le fichier ui/home/HomeScreen.kt et examinez le composable HomeScreen().
@Composable
fun HomeScreen(
    navigateToItemEntry: () -> Unit,
    navigateToItemUpdate: (Int) -> Unit,
    modifier: Modifier = Modifier,
) {
    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()

    Scaffold(
        topBar = {
            // Top app with app title
        },
        floatingActionButton = {
            FloatingActionButton(
                // onClick details
            ) {
                Icon(
                    // Icon details
                )
            }
        },
    ) { innerPadding ->

       // Display List header and List of Items
        HomeBody(
            itemList = listOf(),  // Empty list is being passed in for itemList
            onItemClick = navigateToItemUpdate,
            modifier = modifier.padding(innerPadding)
                              .fillMaxSize()
        )
    }

Cette fonction composable affiche les éléments suivants :

  • La barre d'application supérieure avec le titre de l'application
  • Le bouton d'action flottant qui permet d'ajouter des articles à l'inventaire adc468afa54b6e70.png
  • La fonction composable HomeBody()

La fonction composable HomeBody() affiche les articles d'inventaire en fonction de la liste transmise. Dans le cadre de l'implémentation du code de démarrage, une liste vide (listOf()) est transmise à la fonction composable HomeBody(). Pour transmettre la liste d'inventaire à ce composable, vous devez récupérer les données d'inventaire du dépôt et les transmettre dans HomeViewModel.

Émettre l'état de l'UI dans le HomeViewModel

Lorsque vous avez ajouté des méthodes à ItemDao pour obtenir des articles (getItem() et getAllItems()), vous avez spécifié un Flow en tant que type renvoyé. Rappelez-vous qu'un Flow représente un flux de données générique. En renvoyant un Flow, il vous suffit d'appeler les méthodes à partir du DAO une seule fois pour un cycle de vie donné. Room gère les mises à jour des données sous-jacentes de manière asynchrone.

Lorsque l'on obtient des données à partir d'un flux, on parle de collecte à partir d'un flux. Lors de la collecte à partir d'un flux dans votre couche d'UI, vous devez prendre en considération quelques points importants.

  • Quand des événements de cycle de vie ont lieu, comme les modifications de configuration (rotation de l'appareil, par exemple), l'activité est recréée. Cela entraîne une recomposition et une nouvelle collecte des données à partir de votre Flow.
  • Les valeurs doivent être mises en cache en tant qu'état afin que les données existantes ne soient pas perdues entre les événements de cycle de vie.
  • Les flux doivent être annulés s'il ne reste aucun observateur (par exemple, à la fin du cycle de vie d'un composable).

La méthode recommandée pour exposer un Flow à partir d'un ViewModel consiste à utiliser un StateFlow. En ayant recours à un StateFlow, vous pouvez enregistrer et observer les données, quel que soit le cycle de vie de l'UI. Pour convertir un Flow en StateFlow, utilisez l'opérateur stateIn.

L'opérateur stateIn comporte trois paramètres expliqués ci-dessous :

  • scope : le viewModelScope définit le cycle de vie du StateFlow. Lorsque le viewModelScope est annulé, le StateFlow est également annulé.
  • started : le pipeline ne doit être actif que lorsque l'UI est visible. Pour ce faire, utilisez la méthode SharingStarted.WhileSubscribed(). Pour configurer un délai (en millisecondes) entre la disparition du dernier abonné et l'arrêt de la coroutine de partage, transmettez le TIMEOUT_MILLIS à la méthode SharingStarted.WhileSubscribed().
  • initialValue : définissez la valeur initiale du flux d'état sur HomeUiState().

Une fois que vous avez converti le Flow en StateFlow, vous pouvez le collecter à l'aide de la méthode collectAsState(), en convertissant ses données en State de même type.

Au cours de cette étape, vous allez récupérer tous les articles de la base de données Room sous forme d'API observable StateFlow pour l'état de l'UI. Lorsque les données d'inventaire de Room changent, l'interface utilisateur est automatiquement mise à jour.

  1. Ouvrez le fichier ui/home/HomeViewModel.kt, qui contient une constante TIMEOUT_MILLIS et une classe de données HomeUiState avec une liste d'articles en tant que paramètre constructeur.
// No need to copy over, this code is part of starter code

class HomeViewModel : ViewModel() {

    companion object {
        private const val TIMEOUT_MILLIS = 5_000L
    }
}

data class HomeUiState(val itemList: List<Item> = listOf())
  1. Dans la classe HomeViewModel, déclarez un attribut val nommé homeUiState de type StateFlow<HomeUiState>. Vous corrigerez bientôt l'erreur d'initialisation.
val homeUiState: StateFlow<HomeUiState>
  1. Appelez getAllItemsStream() sur itemsRepository et affectez-le au homeUiState que vous venez de déclarer.
val homeUiState: StateFlow<HomeUiState> =
    itemsRepository.getAllItemsStream()

Vous obtenez maintenant le message d'erreur "Unresolved reference: itemsRepository" (Référence non résolue : itemsRepository). Pour résoudre l'erreur de référence non résolue, vous devez transmettre l'objet ItemsRepository au HomeViewModel.

  1. Ajoutez un paramètre constructeur de type ItemsRepository à la classe HomeViewModel.
import com.example.inventory.data.ItemsRepository

class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
  1. Dans le fichier ui/AppViewModelProvider.kt et dans l'initialiseur HomeViewModel, transmettez l'objet ItemsRepository comme indiqué.
initializer {
    HomeViewModel(inventoryApplication().container.itemsRepository)
}
  1. Revenez au fichier HomeViewModel.kt. Notez l'erreur de correspondance de type. Pour résoudre ce problème, ajoutez un mappage de transformation comme indiqué ci-dessous.
val homeUiState: StateFlow<HomeUiState> =
    itemsRepository.getAllItemsStream().map { HomeUiState(it) }

Android Studio affiche toujours une erreur de correspondance de type. Cette erreur est due au fait que homeUiState est de type StateFlow et que getAllItemsStream() renvoie un Flow.

  1. Utilisez l'opérateur stateIn pour convertir le Flow en StateFlow. StateFlow est l'API observable pour l'état de l'UI, qui permet à l'UI de se mettre à jour.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

val homeUiState: StateFlow<HomeUiState> =
    itemsRepository.getAllItemsStream().map { HomeUiState(it) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
            initialValue = HomeUiState()
        )
  1. Compilez l'application pour vous assurer que le code ne contient pas d'erreurs. Aucune modification ne sera visible.

4. Afficher les données d'inventaire

Dans cette tâche, vous allez collecter et mettre à jour l'état de l'UI dans le HomeScreen.

  1. Dans le fichier HomeScreen.kt et dans la fonction composable HomeScreen, ajoutez un paramètre de type HomeViewModel et initialisez-le.
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider

@Composable
fun HomeScreen(
    navigateToItemEntry: () -> Unit,
    navigateToItemUpdate: (Int) -> Unit,
    modifier: Modifier = Modifier,
    viewModel: HomeViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
  1. Dans la fonction composable HomeScreen, ajoutez un val appelé homeUiState pour collecter l'état de l'UI à partir du HomeViewModel. Vous utiliserez collectAsState(), qui collecte les valeurs de ce StateFlow et représente sa dernière valeur via State.
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue

val homeUiState by viewModel.homeUiState.collectAsState()
  1. Mettez à jour l'appel de fonction HomeBody() et transmettez homeUiState.itemList au paramètre itemList.
HomeBody(
    itemList = homeUiState.itemList,
    onItemClick = navigateToItemUpdate,
    modifier = modifier.padding(innerPadding)
)
  1. Exécutez l'application. Notez que la liste d'inventaire s'affiche si vous avez enregistré des articles dans la base de données de votre application. Si cette liste est vide, ajoutez des articles d'inventaire à la base de données de l'application.

Écran de téléphone avec des articles d'inventaire

5. Tester votre base de données

Dans les précédents ateliers de programmation, nous vous avons expliqué qu'il était important de tester votre code. Dans cette tâche, vous allez ajouter des tests unitaires afin de tester vos requêtes DAO. Vous ajouterez ensuite d'autres tests à mesure que vous avancerez dans l'atelier de programmation.

L'approche recommandée pour tester l'implémentation de votre base de données consiste à écrire un test JUnit exécuté sur un appareil Android. Comme ces tests ne nécessitent pas de créer une activité, ils sont plus rapides à exécuter que les tests d'UI.

  1. Dans le fichier build.gradle.kts (Module :app), notez les dépendances suivantes pour Espresso et JUnit.
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
  1. Passez à la vue Project (Projet), puis effectuez un clic droit sur src > New > Directory (src > Nouveau > Répertoire) pour créer un ensemble de sources pour vos tests.

e53b0f0e0b6aba29.png

  1. Sélectionnez androidTest/kotlin dans le pop-up New Directory (Nouveau répertoire).

860b7e1af5f116a.png

  1. Créez une classe Kotlin appelée ItemDaoTest.kt.
  2. Annotez la classe ItemDaoTest avec @RunWith(AndroidJUnit4::class). Elle devrait maintenant ressembler à l'exemple de code suivant :
package com.example.inventory

import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ItemDaoTest {
}
  1. Dans la classe, ajoutez des variables var privées de type ItemDao et InventoryDatabase.
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao

private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
  1. Ajoutez une fonction pour créer la base de données et l'annoter avec @Before afin qu'elle puisse s'exécuter avant chaque test.
  2. Dans la méthode, initialisez itemDao.
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import org.junit.Before

@Before
fun createDb() {
    val context: Context = ApplicationProvider.getApplicationContext()
    // Using an in-memory database because the information stored here disappears when the
    // process is killed.
    inventoryDatabase = Room.inMemoryDatabaseBuilder(context, InventoryDatabase::class.java)
        // Allowing main thread queries, just for testing.
        .allowMainThreadQueries()
        .build()
    itemDao = inventoryDatabase.itemDao()
}

Dans cette fonction, vous utiliserez une base de données en mémoire sans assurer sa persistance sur le disque. Pour ce faire, vous devez vous servir de la fonction inMemoryDatabaseBuilder(). Vous effectuez cette opération, car la persistance des informations n'est pas nécessaire. À la place, elles doivent être supprimées lorsque le processus est arrêté. Vous exécuterez les requêtes DAO dans le thread principal avec .allowMainThreadQueries(), à des fins de test uniquement.

  1. Ajoutez une autre fonction pour fermer la base de données. Annotez-la avec @After pour fermer la base de données et l'exécuter après chaque test.
import org.junit.After
import java.io.IOException

@After
@Throws(IOException::class)
fun closeDb() {
    inventoryDatabase.close()
}
  1. Déclarez les éléments de la classe ItemDaoTest que la base de données doit utiliser, comme illustré dans l'exemple de code suivant :
import com.example.inventory.data.Item

private var item1 = Item(1, "Apples", 10.0, 20)
private var item2 = Item(2, "Bananas", 15.0, 97)
  1. Ajoutez des fonctions utilitaires pour générer un article, puis deux, dans la base de données. Vous les utiliserez plus tard dans votre test. Marquez-les comme suspend afin qu'ils puissent s'exécuter dans une coroutine.
private suspend fun addOneItemToDb() {
    itemDao.insert(item1)
}

private suspend fun addTwoItemsToDb() {
    itemDao.insert(item1)
    itemDao.insert(item2)
}
  1. Écrivez un test pour insérer un seul article dans la base de données : insert(). Nommez ce test daoInsert_insertsItemIntoDB et annotez-le avec @Test.
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test

@Test
@Throws(Exception::class)
fun daoInsert_insertsItemIntoDB() = runBlocking {
    addOneItemToDb()
    val allItems = itemDao.getAllItems().first()
    assertEquals(allItems[0], item1)
}

Dans ce test, vous utiliserez la fonction utilitaire addOneItemToDb() pour ajouter un article à la base de données. Vous lirez ensuite le premier article de la base de données. Avec assertEquals(), vous comparerez la valeur attendue à la valeur réelle. Vous exécuterez le test dans une nouvelle coroutine avec runBlocking{}. Cette configuration explique pourquoi vous marquez les fonctions utilitaires comme suspend.

  1. Exécutez le test et vérifiez qu'il aboutit.

cd95648114520f13.png

6521e8595bb33a91.png

  1. Écrivez un autre test pour getAllItems() à partir de la base de données. Nommez le test daoGetAllItems_returnsAllItemsFromDB.
@Test
@Throws(Exception::class)
fun daoGetAllItems_returnsAllItemsFromDB() = runBlocking {
    addTwoItemsToDb()
    val allItems = itemDao.getAllItems().first()
    assertEquals(allItems[0], item1)
    assertEquals(allItems[1], item2)
}

Dans le test ci-dessus, deux articles sont ajoutés à la base de données dans une coroutine. Vous devez ensuite lire ces deux articles et les comparer aux valeurs attendues.

6. Afficher les détails de l'article

Dans cette tâche, vous allez lire et afficher les détails de l'entité sur l'écran Item Details (Détails de l'article). Vous utiliserez l'état d'UI de l'article (nom, prix ou quantité, par exemple) dans la base de données de l'application Inventory et vous l'afficherez sur l'écran Item Details (Détails de l'article) avec le composable ItemDetailsScreen. La fonction composable ItemDetailsScreen est prédéfinie pour vous et contient trois composables Text qui affichent les détails de l'article.

ui/item/ItemDetailsScreen.kt

Cet écran fait partie du code de démarrage et affiche les détails des éléments, que vous découvrirez dans un prochain atelier de programmation. Vous ne travaillerez pas sur cet écran dans cet atelier de programmation. ItemDetailsViewModel.kt est le ViewModel correspondant à cet écran.

a5009ad021b830ff.png

  1. Dans la fonction composable HomeScreen, vous remarquerez l'appel de fonction HomeBody(). navigateToItemUpdate est transmis au paramètre onItemClick, qui est appelé lorsque vous cliquez sur un article de votre liste.
// No need to copy over
HomeBody(
    itemList = homeUiState.itemList,
    onItemClick = navigateToItemUpdate,
    modifier = modifier
        .padding(innerPadding)
        .fillMaxSize()
)
  1. Ouvrez ui/navigation/InventoryNavGraph.kt et notez le paramètre navigateToItemUpdate dans le composable HomeScreen. Ce paramètre spécifie la destination de la navigation comme écran "Item Details" (Détails de l'article).
// No need to copy over
HomeScreen(
    navigateToItemEntry = { navController.navigate(ItemEntryDestination.route) },
    navigateToItemUpdate = {
        navController.navigate("${ItemDetailsDestination.route}/${it}")
   }

Cette partie de la fonctionnalité onItemClick est déjà implémentée pour vous. Lorsque vous cliquez sur l'article de la liste, l'application accède à l'écran "Item Details" (Détails de l'article).

  1. Cliquez sur un article de la liste d'inventaire pour afficher l'écran "Item Details" (Détails de l'article) avec des champs vides.

Écran "Item Details" (Détails de l'article) avec des champs vides

Pour renseigner les champs de texte avec les détails de l'article, vous devez collecter l'état de l'UI dans ItemDetailsScreen().

  1. Dans UI/Item/ItemDetailsScreen.kt, ajoutez un paramètre au composable ItemDetailsScreen de type ItemDetailsViewModel et initialisez-le à l'aide de la méthode de fabrique.
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider

@Composable
fun ItemDetailsScreen(
    navigateToEditItem: (Int) -> Unit,
    navigateBack: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: ItemDetailsViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
  1. Dans le composable ItemDetailsScreen(), créez un val appelé uiState pour collecter l'état de l'UI. Utilisez collectAsState() pour collecter uiState StateFlow et représenter sa dernière valeur via State. Android Studio affiche une erreur de référence non résolue.
import androidx.compose.runtime.collectAsState

val uiState = viewModel.uiState.collectAsState()
  1. Pour résoudre l'erreur, créez un val nommé uiState de type StateFlow<ItemDetailsUiState> dans la classe ItemDetailsViewModel.
  2. Récupérez les données du référentiel d'articles, puis mappez-les à ItemDetailsUiState à l'aide de la fonction d'extension toItemDetails(). Item.toItemDetails() est déjà écrite dans le code de démarrage.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

val uiState: StateFlow<ItemDetailsUiState> =
         itemsRepository.getItemStream(itemId)
             .filterNotNull()
             .map {
                 ItemDetailsUiState(itemDetails = it.toItemDetails())
             }.stateIn(
                 scope = viewModelScope,
                 started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
                 initialValue = ItemDetailsUiState()
             )
  1. Transmettez ItemsRepository dans ItemDetailsViewModel pour résoudre l'erreur Unresolved reference: itemsRepository.
class ItemDetailsViewModel(
    savedStateHandle: SavedStateHandle,
    private val itemsRepository: ItemsRepository
    ) : ViewModel() {
  1. Dans ui/AppViewModelProvider.kt, mettez à jour l'initialiseur pour ItemDetailsViewModel, comme indiqué dans l'extrait de code suivant :
initializer {
    ItemDetailsViewModel(
        this.createSavedStateHandle(),
        inventoryApplication().container.itemsRepository
    )
}
  1. Revenez à ItemDetailsScreen.kt. Vous remarquerez que l'erreur dans le composable ItemDetailsScreen() est résolue.
  2. Dans le composable ItemDetailsScreen(), mettez à jour l'appel de fonction ItemDetailsBody() et transmettez uiState.value à l'argument itemUiState.
ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = {  },
    onDelete = { },
    modifier = modifier.padding(innerPadding)
)
  1. Observez les implémentations d'ItemDetailsBody() et d'ItemInputForm(). Vous transmettez l'item actuellement sélectionné d'ItemDetailsBody() à ItemDetails().
// No need to copy over

@Composable
private fun ItemDetailsBody(
    itemUiState: ItemUiState,
    onSellItem: () -> Unit,
    onDelete: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
       //...
    ) {
        var deleteConfirmationRequired by rememberSaveable { mutableStateOf(false) }
        ItemDetails(
             item = itemDetailsUiState.itemDetails.toItem(), modifier = Modifier.fillMaxWidth()
         )

      //...
    }
  1. Exécutez l'application. Lorsque vous cliquez sur un article de la liste sur l'écran Inventory (Inventaire), l'écran Item Details (Détails de l'article) s'affiche.
  2. Notez que l'écran n'est plus vide. Il affiche les détails de l'entité extraits de la base de données d'inventaire.

Écran "Item Details" (Détails de l'article) avec les détails de l'article renseignés

  1. Appuyez sur le bouton Sell (Vendre). Rien ne se passe !

Dans la section suivante, vous allez implémenter la fonctionnalité du bouton Sell (Vendre).

7. Implémenter l'écran "Item Details" (Détails de l'article)

ui/item/ItemEditScreen.kt

L'écran de modification d'article vous est déjà fourni dans le code de démarrage.

Cette mise en page contient des composables de champ de texte permettant de modifier les détails de n'importe quel nouvel article d'inventaire.

Écran "Edit item" (Modifier l'article) avec des éléments vides

Le code de cette application n'est toujours pas fonctionnel. Par exemple, sur l'écran Item Details (Détails de l'article), lorsque vous appuyez sur le bouton Sell (Vendre), la quantité en stock ne diminue pas. Lorsque vous appuyez sur le bouton Delete (Supprimer), une boîte de dialogue de confirmation s'affiche. Toutefois, lorsque vous sélectionnez le bouton Yes (Oui), l'application ne supprime pas l'article.

Pop-up de confirmation de suppression d'article

Enfin, le bouton d'action flottant be6c7ed4ac207351.png ouvre un écran Edit Item (Modifier l'article) vide.

Écran "Edit item" (Modifier l'article) avec des éléments vides

Dans cette section, vous allez implémenter les fonctionnalités des boutons Sell (Vendre) et Delete (Supprimer), ainsi que celles des boutons d'action flottants.

8. Implémenter la fonctionnalité de vente d'un article

Dans cette section, vous allez étendre les fonctionnalités de l'application en implémentant la fonctionnalité de vente. Cette mise à jour implique les tâches suivantes :

  • Ajouter un test pour que la fonction DAO mette à jour une entité
  • Ajouter une fonction dans le ItemDetailsViewModel pour réduire la quantité et mettre à jour l'entité dans la base de données de l'application
  • Désactiver le bouton Sell (Vendre) si la quantité est nulle
  1. Dans ItemDaoTest.kt, ajoutez une fonction appelée daoUpdateItems_updatesItemsInDB() sans paramètre. Annotez-la avec @Test et @Throws(Exception::class).
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
  1. Définissez la fonction et créez un bloc runBlocking. Appelez-y addTwoItemsToDb().
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
    addTwoItemsToDb()
}
  1. Mettez à jour les deux entités avec des valeurs différentes, en appelant itemDao.update.
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
  1. Récupérez les entités avec itemDao.getAllItems(). Comparez-les à l'entité mise à jour et confirmez-les.
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
  1. Assurez-vous que la fonction terminée ressemble à ceci :
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
    addTwoItemsToDb()
    itemDao.update(Item(1, "Apples", 15.0, 25))
    itemDao.update(Item(2, "Bananas", 5.0, 50))

    val allItems = itemDao.getAllItems().first()
    assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
    assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
}
  1. Exécutez le test et vérifiez qu'il aboutit.

Ajouter une fonction dans le ViewModel

  1. Dans ItemDetailsViewModel.kt et dans la classe ItemDetailsViewModel, ajoutez une fonction appelée reduceQuantityByOne() sans paramètre.
fun reduceQuantityByOne() {
}
  1. Dans cette fonction, lancez une coroutine avec viewModelScope.launch{}.
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope

viewModelScope.launch {
}
  1. Dans le bloc launch, créez un attribut val nommé currentItem et définissez-le sur uiState.value.toItem().
val currentItem = uiState.value.toItem()

L'uiState.value est de type ItemUiState. Vous devez convertir cette valeur en type d'entité Item avec la fonction d'extension toItem().

  1. Ajoutez une instruction if pour vérifier si la valeur quality est supérieure à 0.
  2. Appelez updateItem() sur itemsRepository et transmettez le currentItem mis à jour. Utilisez copy() pour mettre à jour la valeur quantity afin que la fonction se présente comme suit :
fun reduceQuantityByOne() {
    viewModelScope.launch {
        val currentItem = uiState.value.itemDetails.toItem()
        if (currentItem.quantity > 0) {
    itemsRepository.updateItem(currentItem.copy(quantity = currentItem.quantity - 1))
       }
    }
}
  1. Revenez à ItemDetailsScreen.kt.
  2. Dans le composable ItemDetailsScreen, accédez à l'appel de fonction ItemDetailsBody().
  3. Dans le lambda onSellItem, appelez viewModel.reduceQuantityByOne().
ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = { viewModel.reduceQuantityByOne() },
    onDelete = { },
    modifier = modifier.padding(innerPadding)
)
  1. Exécutez l'application.
  2. Sur l'écran Inventory (Inventaire), cliquez sur un élément de la liste. Lorsque l'écran Item Details (Détails de l'article) s'affiche, appuyez sur Sell (Vendre). Vous remarquerez qu'il y a désormais un article en moins dans le champ "Quantity in Stock" (Quantité en stock).

L'écran "Item Details" (Détails de l'article) réduit la quantité d'un article lorsque l'utilisateur appuie sur le bouton "Sell" (Vendre)

  1. Sur l'écran Item Details (Détails de l'article), appuyez de manière continue sur le bouton Sell (Vendre) jusqu'à ce que la quantité soit nulle.

Lorsque la quantité est nulle, appuyez à nouveau sur Sell (Vendre). Aucune modification n'est visible, car la fonction reduceQuantityByOne() vérifie si la quantité est supérieure à zéro avant de la mettre à jour.

Écran "Item Details" (Détails de l'article) avec la quantité en stock disponible affichant 0

Pour optimiser l'interaction des utilisateurs avec l'application, vous pouvez désactiver le bouton Sell (Vendre) lorsqu'il n'y a pas d'article à vendre.

  1. Dans la classe ItemDetailsViewModel, définissez la valeur outOfStock en fonction de it.quantity dans la transformation map.
val uiState: StateFlow<ItemDetailsUiState> =
    itemsRepository.getItemStream(itemId)
        .filterNotNull()
        .map {
            ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails())
        }.stateIn(
            //...
        )
  1. Exécutez votre application. Vous remarquerez qu'elle désactive le bouton Sell (Vendre) lorsque la quantité en stock est nulle.

Écran "Item Details" (Détails de l'article) avec le bouton "Sell" (Vendre) désactivé

Félicitations ! Vous avez implémenté la fonctionnalité de vente d'articles dans votre application.

Supprimer une entité d'article

Comme pour la tâche précédente, vous devez étendre les fonctionnalités de votre application en implémentant la fonction permettant de supprimer un article, ce qui est beaucoup plus facile à exécuter que la fonctionnalité de vente. Ce processus implique les tâches suivantes :

  • Ajouter un test pour la requête DAO de suppression
  • Ajouter une fonction dans la classe ItemDetailsViewModel pour supprimer une entité de la base de données
  • Mettre à jour le composable ItemDetailsBody

Ajouter un test DAO

  1. Dans ItemDaoTest.kt, ajoutez un test appelé daoDeleteItems_deletesAllItemsFromDB().
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB()
  1. Lancez une coroutine avec runBlocking {}.
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
}
  1. Ajoutez deux articles à la base de données et appelez itemDao.delete() au niveau de ces deux articles pour les supprimer de la base de données.
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
  1. Récupérez les entités de la base de données et vérifiez que la liste est vide. Le test terminé devrait se présenter comme suit :
import org.junit.Assert.assertTrue

@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
    addTwoItemsToDb()
    itemDao.delete(item1)
    itemDao.delete(item2)
    val allItems = itemDao.getAllItems().first()
    assertTrue(allItems.isEmpty())
}

Ajouter une fonction de suppression dans le ItemDetailsViewModel

  1. Dans le ItemDetailsViewModel, ajoutez une fonction nommée deleteItem() qui ne reçoit aucun paramètre et ne renvoie rien.
  2. Dans la fonction deleteItem(), ajoutez un appel de fonction itemsRepository.deleteItem() et transmettez uiState.value.toItem().
suspend fun deleteItem() {
    itemsRepository.deleteItem(uiState.value.itemDetails.toItem())
}

Dans cette fonction, vous allez convertir l'uiState du type itemDetails en type d'entité Item à l'aide de la fonction d'extension toItem().

  1. Dans le composable ui/item/ItemDetailsScreen, ajoutez un val appelé coroutineScope et définissez-le sur rememberCoroutineScope(). Cette approche renvoie un champ d'application de coroutine lié à la composition où elle est appelée (composable ItemDetailsScreen).
import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. Faites défiler la page jusqu'à la fonction ItemDetailsBody().
  2. Lancez une coroutine avec coroutineScope dans le lambda onDelete.
  3. Dans le bloc launch, appelez la méthode deleteItem() sur viewModel.
import kotlinx.coroutines.launch

ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = { viewModel.reduceQuantityByOne() },
    onDelete = {
        coroutineScope.launch {
           viewModel.deleteItem()
    }
    modifier = modifier.padding(innerPadding)
)
  1. Une fois l'article supprimé, revenez à l'écran d'inventaire.
  2. Appelez navigateBack() après l'appel de fonction deleteItem().
onDelete = {
    coroutineScope.launch {
        viewModel.deleteItem()
        navigateBack()
    }
  1. Toujours dans le fichier ItemDetailsScreen.kt, faites défiler la page jusqu'à la fonction ItemDetailsBody().

Cette fonction fait partie du code de démarrage. Ce composable affiche une boîte de dialogue d'alerte pour obtenir la confirmation de l'utilisateur avant de supprimer l'article et appelle la fonction deleteItem() lorsque vous appuyez sur Yes (Oui).

// No need to copy over

@Composable
private fun ItemDetailsBody(
    itemUiState: ItemUiState,
    onSellItem: () -> Unit,
    onDelete: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        /*...*/
    ) {
        //...

        if (deleteConfirmationRequired) {
            DeleteConfirmationDialog(
                onDeleteConfirm = {
                    deleteConfirmationRequired = false
                    onDelete()
                },
                //...
            )
        }
    }
}

Lorsque vous appuyez sur No (Non), l'application ferme la boîte de dialogue d'alerte. La fonction showConfirmationDialog() affiche l'alerte suivante :

Pop-up de confirmation de suppression d'article

  1. Exécutez l'application.
  2. Sélectionnez un article de la liste sur l'écran Inventory (Inventaire).
  3. Sur l'écran Item Details (Détails de l'article), appuyez sur Delete (Supprimer).
  4. Appuyez sur Yes (Oui) dans la boîte de dialogue d'alerte. L'application revient à l'écran Inventory (Inventaire).
  5. Confirmez que l'entité que vous avez supprimée ne figure plus dans la base de données de l'application.

Félicitations ! Vous avez implémenté la fonctionnalité de suppression.

Écran "Item Details" (Détails de l'article) avec la boîte de dialogue d'alerte

Écran "Inventory" (Inventaire) sans l'article supprimé

Modifier l'entité de l'élément

Comme dans les sections précédentes, vous allez ajouter dans l'application une autre amélioration qui modifiera l'entité d'un article.

Voici une rapide procédure à suivre pour modifier une entité dans la base de données de l'application :

  • Ajoutez un test à la requête DAO visant à tester l'obtention de l'article.
  • Renseignez les champs de texte et l'écran Edit Item (Modifier l'article) avec les détails de l'entité.
  • Mettez à jour l'entité dans la base de données à l'aide de Room.

Ajouter un test DAO

  1. Dans ItemDaoTest.kt, ajoutez un test appelé daoGetItem_returnsItemFromDB().
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB()
  1. Définissez la fonction. Dans la coroutine, ajoutez un article à la base de données.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
    addOneItemToDb()
}
  1. Récupérez l'entité de la base de données à l'aide de la fonction itemDao.getItem() et définissez-la sur un val nommé item.
val item = itemDao.getItem(1)
  1. Comparez la valeur réelle à la valeur récupérée, puis confirmez le résultat avec assertEquals(). Le test terminé devrait se présenter comme suit :
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
    addOneItemToDb()
    val item = itemDao.getItem(1)
    assertEquals(item.first(), item1)
}
  1. Exécutez le test et assurez-vous qu'il aboutit.

Remplir les champs de texte

Si vous exécutez l'application, accédez à l'écran Item Details (Détails de l'article), puis cliquez sur le bouton d'action flottant. Vous verrez que l'écran s'intitule désormais Edit Item (Modifier l'article). Cependant, tous les champs de texte sont vides. Au cours de cette étape, vous allez renseigner les champs de texte de l'écran Edit Item (Modifier l'article) avec les informations concernant l'entité.

Écran "Item Details" (Détails de l'article) avec le bouton "Sell" (Vendre) désactivé

Écran "Edit item" (Modifier l'article) avec des champs vides

  1. Dans ItemDetailsScreen.kt, faites défiler la page jusqu'au composable ItemDetailsScreen.
  2. Dans FloatingActionButton(), modifiez l'argument onClick pour inclure uiState.value.itemDetails.id, qui est l'id de l'entité sélectionnée. Utilisez cet id pour récupérer les détails de l'entité.
FloatingActionButton(
    onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
    modifier = /*...*/
)
  1. Dans la classe ItemEditViewModel, ajoutez un bloc init.
init {

}
  1. Dans le bloc init, lancez une coroutine avec viewModelScope.launch.
import kotlinx.coroutines.launch

viewModelScope.launch { }
  1. Toujours dans le bloc launch, récupérez les détails de l'entité avec itemsRepository.getItemStream(itemId).
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first

init {
    viewModelScope.launch {
        itemUiState = itemsRepository.getItemStream(itemId)
            .filterNotNull()
            .first()
            .toItemUiState(true)
    }
}

Dans ce bloc de lancement, ajoutez un filtre pour renvoyer un flux qui ne contiendra que les valeurs non nulles. Avec toItemUiState(), convertissez l'entité item en ItemUiState. Transmettez la valeur actionEnabled en tant que true pour activer le bouton Save (Enregistrer).

Pour résoudre l'erreur Unresolved reference: itemsRepository, vous devez transmettre ItemsRepository en tant que dépendance au modèle de vue.

  1. Ajoutez un paramètre constructeur à la classe ItemEditViewModel.
class ItemEditViewModel(
    savedStateHandle: SavedStateHandle,
    private val itemsRepository: ItemsRepository
)
  1. Dans le fichier AppViewModelProvider.kt et dans l'initialiseur ItemEditViewModel, ajoutez l'objet ItemsRepository en tant qu'argument.
initializer {
    ItemEditViewModel(
        this.createSavedStateHandle(),
        inventoryApplication().container.itemsRepository
    )
}
  1. Exécutez l'application.
  2. Accédez à Item Details (Détails de l'article) et appuyez sur le bouton d'action flottant 2ae4a1588eba091b.png.
  3. Vous remarquerez que les champs contiennent désormais les détails de l'article.
  4. Modifiez la quantité en stock ou tout autre champ, puis appuyez sur Save (Enregistrer).

Rien ne se passe ! C'est normal, vous ne mettez pas à jour l'entité dans la base de données de l'application. Vous résoudrez ce problème dans la section suivante.

Écran "Item Details" (Détails de l'article) avec le bouton "Sell" (Vendre) désactivé

Écran "Edit item" (Modifier l'article) avec un champ vide

Mettre à jour l'entité à l'aide de Room

Dans cette dernière tâche, vous allez ajouter les dernières parties du code pour implémenter la fonctionnalité de mise à jour. Vous allez définir les fonctions nécessaires dans le ViewModel et les utiliser dans l'ItemEditScreen.

Et c'est reparti pour coder !

  1. Dans la classe ItemEditViewModel, ajoutez une fonction appelée updateUiState() qui reçoit un objet ItemUiState et ne renvoie rien. Cette fonction met à jour l'itemUiState avec les nouvelles valeurs saisies par l'utilisateur.
fun updateUiState(itemDetails: ItemDetails) {
    itemUiState =
        ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails))
}

Dans cette fonction, attribuez l'élément itemDetails transmis à itemUiState et mettez à jour la valeur isEntryValid. L'application active le bouton Save (Enregistrer) si itemDetails est true. Définissez cette valeur sur true seulement si l'entrée saisie par l'utilisateur est valide.

  1. Accédez au fichier ItemEditScreen.kt.
  2. Dans le composable ItemEditScreen, faites défiler la page jusqu'à l'appel de fonction ItemEntryBody().
  3. Définissez la valeur de l'argument onItemValueChange sur la nouvelle fonction updateUiState.
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = { },
    modifier = modifier.padding(innerPadding)
)
  1. Exécutez l'application.
  2. Accédez à l'écran Edit Item (Modifier l'article).
  3. Faites en sorte qu'une des valeurs d'entité soit vide pour qu'elle ne soit pas valide. Notez que le bouton Save (Enregistrer) se désactive automatiquement.

Écran "Item Details" (Détails de l'article) avec le bouton "Sell" (Vendre) activé

Écran "Edit Item" (Modifier l'article) avec tous les champs de texte renseignés et le bouton "Save" (Enregistrer) activé

Écran "Edit Item" (Modifier l'article) avec le bouton "Save" (Enregistrer) désactivé

  1. Revenez à la classe ItemEditViewModel et ajoutez une fonction suspend appelée updateItem() sans élément d'entrée. Cette fonction vous permettra d'enregistrer l'entité mise à jour dans la base de données Room.
suspend fun updateItem() {
}
  1. Dans la fonction getUpdatedItemEntry(), ajoutez une condition if pour valider l'entrée utilisateur en appelant la fonction validateInput().
  2. Appelez la fonction updateItem() sur itemsRepository en transmettant itemUiState.itemDetails.toItem(). Les entités pouvant être ajoutées à la base de données Room doivent être de type Item. La fonction terminée devrait se présenter comme suit :
suspend fun updateItem() {
    if (validateInput(itemUiState.itemDetails)) {
        itemsRepository.updateItem(itemUiState.itemDetails.toItem())
    }
}
  1. Revenez au composable ItemEditScreen. Vous avez besoin d'un champ d'application de coroutine pour appeler la fonction updateItem(). Créez une valeur appelée coroutineScope et définissez-la sur rememberCoroutineScope().
import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. Dans l'appel de fonction ItemEntryBody(), mettez à jour l'argument de fonction onSaveClick pour démarrer une coroutine dans coroutineScope.
  2. Dans le bloc launch, appelez updateItem() sur viewModel et revenez en arrière.
import kotlinx.coroutines.launch

onSaveClick = {
    coroutineScope.launch {
        viewModel.updateItem()
        navigateBack()
    }
},

L'appel de fonction ItemEntryBody() terminé devrait se présenter comme suit :

ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = {
        coroutineScope.launch {
            viewModel.updateItem()
            navigateBack()
        }
    },
    modifier = modifier.padding(innerPadding)
)
  1. Exécutez l'application et essayez de modifier les articles de l'inventaire. Vous pouvez désormais modifier n'importe quel article dans la base de données de l'application Inventory.

Écran "Edit Item" (Modifier l'article) avec les détails de l'article modifiés

Écran "Item Details" (Détails de l'article) avec les détails de l'article mis à jour

Félicitations ! Vous avez créé votre première application qui utilise Room pour gérer la base de données.

9. Code de solution

Le code de la solution de cet atelier de programmation se trouve dans le dépôt et la branche GitHub indiqués ci-dessous :

10. En savoir plus

Documentation pour les développeurs Android

Références Kotlin