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.
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.
Tutoriel de la fonction composable HomeScreen
- Ouvrez le fichier
ui/home/HomeScreen.kt
et examinez le composableHomeScreen()
.
@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
- 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
: leviewModelScope
définit le cycle de vie duStateFlow
. Lorsque leviewModelScope
est annulé, leStateFlow
est également annulé.started
: le pipeline ne doit être actif que lorsque l'UI est visible. Pour ce faire, utilisez la méthodeSharingStarted.WhileSubscribed()
. Pour configurer un délai (en millisecondes) entre la disparition du dernier abonné et l'arrêt de la coroutine de partage, transmettez leTIMEOUT_MILLIS
à la méthodeSharingStarted.WhileSubscribed()
.initialValue
: définissez la valeur initiale du flux d'état surHomeUiState()
.
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.
- Ouvrez le fichier
ui/home/HomeViewModel.kt
, qui contient une constanteTIMEOUT_MILLIS
et une classe de donnéesHomeUiState
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())
- Dans la classe
HomeViewModel
, déclarez un attributval
nomméhomeUiState
de typeStateFlow<HomeUiState>
. Vous corrigerez bientôt l'erreur d'initialisation.
val homeUiState: StateFlow<HomeUiState>
- Appelez
getAllItemsStream()
suritemsRepository
et affectez-le auhomeUiState
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
.
- Ajoutez un paramètre constructeur de type
ItemsRepository
à la classeHomeViewModel
.
import com.example.inventory.data.ItemsRepository
class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
- Dans le fichier
ui/AppViewModelProvider.kt
et dans l'initialiseurHomeViewModel
, transmettez l'objetItemsRepository
comme indiqué.
initializer {
HomeViewModel(inventoryApplication().container.itemsRepository)
}
- 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
.
- Utilisez l'opérateur
stateIn
pour convertir leFlow
enStateFlow
.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()
)
- 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
.
- Dans le fichier
HomeScreen.kt
et dans la fonction composableHomeScreen
, ajoutez un paramètre de typeHomeViewModel
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)
)
- Dans la fonction composable
HomeScreen
, ajoutez unval
appeléhomeUiState
pour collecter l'état de l'UI à partir duHomeViewModel
. Vous utiliserezcollectAsState
()
, qui collecte les valeurs de ceStateFlow
et représente sa dernière valeur viaState
.
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
val homeUiState by viewModel.homeUiState.collectAsState()
- Mettez à jour l'appel de fonction
HomeBody()
et transmettezhomeUiState.itemList
au paramètreitemList
.
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding)
)
- 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.
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.
- 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")
- 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.
- Sélectionnez androidTest/kotlin dans le pop-up New Directory (Nouveau répertoire).
- Créez une classe Kotlin appelée
ItemDaoTest.kt
. - 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 {
}
- Dans la classe, ajoutez des variables
var
privées de typeItemDao
etInventoryDatabase
.
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao
private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
- 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. - 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.
- 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()
}
- 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)
- 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)
}
- Écrivez un test pour insérer un seul article dans la base de données :
insert()
. Nommez ce testdaoInsert_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
.
- Exécutez le test et vérifiez qu'il aboutit.
- Écrivez un autre test pour
getAllItems()
à partir de la base de données. Nommez le testdaoGetAllItems_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.
- Dans la fonction composable
HomeScreen
, vous remarquerez l'appel de fonctionHomeBody()
.navigateToItemUpdate
est transmis au paramètreonItemClick
, 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()
)
- Ouvrez
ui/navigation/InventoryNavGraph.kt
et notez le paramètrenavigateToItemUpdate
dans le composableHomeScreen
. 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).
- Cliquez sur un article de la liste d'inventaire pour afficher l'é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()
.
- Dans
UI/Item/ItemDetailsScreen.kt
, ajoutez un paramètre au composableItemDetailsScreen
de typeItemDetailsViewModel
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)
)
- Dans le composable
ItemDetailsScreen()
, créez unval
appeléuiState
pour collecter l'état de l'UI. UtilisezcollectAsState()
pour collecteruiState
StateFlow
et représenter sa dernière valeur viaState
. Android Studio affiche une erreur de référence non résolue.
import androidx.compose.runtime.collectAsState
val uiState = viewModel.uiState.collectAsState()
- Pour résoudre l'erreur, créez un
val
nomméuiState
de typeStateFlow<ItemDetailsUiState>
dans la classeItemDetailsViewModel
. - Récupérez les données du référentiel d'articles, puis mappez-les à
ItemDetailsUiState
à l'aide de la fonction d'extensiontoItemDetails()
.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()
)
- Transmettez
ItemsRepository
dansItemDetailsViewModel
pour résoudre l'erreurUnresolved reference: itemsRepository
.
class ItemDetailsViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
) : ViewModel() {
- Dans
ui/AppViewModelProvider.kt
, mettez à jour l'initialiseur pourItemDetailsViewModel
, comme indiqué dans l'extrait de code suivant :
initializer {
ItemDetailsViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- Revenez à
ItemDetailsScreen.kt
. Vous remarquerez que l'erreur dans le composableItemDetailsScreen()
est résolue. - Dans le composable
ItemDetailsScreen()
, mettez à jour l'appel de fonctionItemDetailsBody()
et transmettezuiState.value
à l'argumentitemUiState
.
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- 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()
)
//...
}
- 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.
- 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.
- 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.
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.
Enfin, le bouton d'action flottant ouvre un écran Edit Item (Modifier l'article) vide.
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
- Dans
ItemDaoTest.kt
, ajoutez une fonction appeléedaoUpdateItems_updatesItemsInDB()
sans paramètre. Annotez-la avec@Test
et@Throws(Exception::class)
.
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
- Définissez la fonction et créez un bloc
runBlocking
. Appelez-yaddTwoItemsToDb()
.
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
addTwoItemsToDb()
}
- 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))
- 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))
- 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))
}
- Exécutez le test et vérifiez qu'il aboutit.
Ajouter une fonction dans le ViewModel
- Dans
ItemDetailsViewModel.kt
et dans la classeItemDetailsViewModel
, ajoutez une fonction appeléereduceQuantityByOne()
sans paramètre.
fun reduceQuantityByOne() {
}
- Dans cette fonction, lancez une coroutine avec
viewModelScope.launch{}
.
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope
viewModelScope.launch {
}
- Dans le bloc
launch
, créez un attributval
nommécurrentItem
et définissez-le suruiState.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
()
.
- Ajoutez une instruction
if
pour vérifier si la valeurquality
est supérieure à0
. - Appelez
updateItem()
suritemsRepository
et transmettez lecurrentItem
mis à jour. Utilisezcopy()
pour mettre à jour la valeurquantity
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))
}
}
}
- Revenez à
ItemDetailsScreen.kt
. - Dans le composable
ItemDetailsScreen
, accédez à l'appel de fonctionItemDetailsBody()
. - Dans le lambda
onSellItem
, appelezviewModel.reduceQuantityByOne()
.
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- Exécutez l'application.
- 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).
- 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.
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.
- Dans la classe
ItemDetailsViewModel
, définissez la valeuroutOfStock
en fonction deit
.quantity
dans la transformationmap
.
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails())
}.stateIn(
//...
)
- Exécutez votre application. Vous remarquerez qu'elle désactive le bouton Sell (Vendre) lorsque la quantité en stock est nulle.
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
- Dans
ItemDaoTest.kt
, ajoutez un test appelédaoDeleteItems_deletesAllItemsFromDB()
.
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB()
- Lancez une coroutine avec
runBlocking {}
.
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
}
- 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)
- 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
- Dans le
ItemDetailsViewModel
, ajoutez une fonction nomméedeleteItem()
qui ne reçoit aucun paramètre et ne renvoie rien. - Dans la fonction
deleteItem()
, ajoutez un appel de fonctionitemsRepository.deleteItem()
et transmettezuiState.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
()
.
- Dans le composable
ui/item/ItemDetailsScreen
, ajoutez unval
appelécoroutineScope
et définissez-le surrememberCoroutineScope()
. Cette approche renvoie un champ d'application de coroutine lié à la composition où elle est appelée (composableItemDetailsScreen
).
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- Faites défiler la page jusqu'à la fonction
ItemDetailsBody()
. - Lancez une coroutine avec
coroutineScope
dans le lambdaonDelete
. - Dans le bloc
launch
, appelez la méthodedeleteItem()
surviewModel
.
import kotlinx.coroutines.launch
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
}
modifier = modifier.padding(innerPadding)
)
- Une fois l'article supprimé, revenez à l'écran d'inventaire.
- Appelez
navigateBack()
après l'appel de fonctiondeleteItem()
.
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
navigateBack()
}
- Toujours dans le fichier
ItemDetailsScreen.kt
, faites défiler la page jusqu'à la fonctionItemDetailsBody()
.
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 :
- Exécutez l'application.
- Sélectionnez un article de la liste sur l'écran Inventory (Inventaire).
- Sur l'écran Item Details (Détails de l'article), appuyez sur Delete (Supprimer).
- Appuyez sur Yes (Oui) dans la boîte de dialogue d'alerte. L'application revient à l'écran Inventory (Inventaire).
- 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.
Modifier l'entité de l'article
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
- Dans
ItemDaoTest.kt
, ajoutez un test appelédaoGetItem_returnsItemFromDB()
.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB()
- Définissez la fonction. Dans la coroutine, ajoutez un article à la base de données.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
}
- Récupérez l'entité de la base de données à l'aide de la fonction
itemDao.getItem()
et définissez-la sur unval
nomméitem
.
val item = itemDao.getItem(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)
}
- 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é.
- Dans
ItemDetailsScreen.kt
, faites défiler la page jusqu'au composableItemDetailsScreen
. - Dans
FloatingActionButton()
, modifiez l'argumentonClick
pour inclureuiState.value.itemDetails.id
, qui est l'id
de l'entité sélectionnée. Utilisez cetid
pour récupérer les détails de l'entité.
FloatingActionButton(
onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
modifier = /*...*/
)
- Dans la classe
ItemEditViewModel
, ajoutez un blocinit
.
init {
}
- Dans le bloc
init
, lancez une coroutine avecviewModelScope
.
launch
.
import kotlinx.coroutines.launch
viewModelScope.launch { }
- Toujours dans le bloc
launch
, récupérez les détails de l'entité avecitemsRepository.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.
- Ajoutez un paramètre constructeur à la classe
ItemEditViewModel
.
class ItemEditViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
)
- Dans le fichier
AppViewModelProvider.kt
et dans l'initialiseurItemEditViewModel
, ajoutez l'objetItemsRepository
en tant qu'argument.
initializer {
ItemEditViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- Exécutez l'application.
- Accédez à Item Details (Détails de l'article) et appuyez sur le bouton d'action flottant .
- Vous remarquerez que les champs contiennent désormais les détails de l'article.
- 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.
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 !
- Dans la classe
ItemEditViewModel
, ajoutez une fonction appeléeupdateUiState()
qui reçoit un objetItemUiState
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.
- Accédez au fichier
ItemEditScreen.kt
. - Dans le composable
ItemEditScreen
, faites défiler la page jusqu'à l'appel de fonctionItemEntryBody()
. - Définissez la valeur de l'argument
onItemValueChange
sur la nouvelle fonctionupdateUiState
.
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = { },
modifier = modifier.padding(innerPadding)
)
- Exécutez l'application.
- Accédez à l'écran Edit Item (Modifier l'article).
- 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.
- Revenez à la classe
ItemEditViewModel
et ajoutez une fonctionsuspend
appeléeupdateItem()
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() {
}
- Dans la fonction
getUpdatedItemEntry()
, ajoutez une conditionif
pour valider l'entrée utilisateur en appelant la fonctionvalidateInput()
. - Appelez la fonction
updateItem()
suritemsRepository
en transmettantitemUiState.itemDetails.
toItem
()
. Les entités pouvant être ajoutées à la base de données Room doivent être de typeItem
. La fonction terminée devrait se présenter comme suit :
suspend fun updateItem() {
if (validateInput(itemUiState.itemDetails)) {
itemsRepository.updateItem(itemUiState.itemDetails.toItem())
}
}
- Revenez au composable
ItemEditScreen
. Vous avez besoin d'un champ d'application de coroutine pour appeler la fonctionupdateItem()
. Créez une valeur appeléecoroutineScope
et définissez-la surrememberCoroutineScope()
.
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- Dans l'appel de fonction
ItemEntryBody()
, mettez à jour l'argument de fonctiononSaveClick
pour démarrer une coroutine danscoroutineScope
. - Dans le bloc
launch
, appelezupdateItem()
surviewModel
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)
)
- 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.
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
- Déboguer votre base de données avec l'outil d'inspection de bases de données
- Enregistrer des données dans une base de données locale à l'aide de Room
- Tester et déboguer votre base de données | Développeurs Android
Références Kotlin