From 7052a52779bb2bf074b981fc3984dfd310a88af9 Mon Sep 17 00:00:00 2001 From: Chocolaterie <110991127+Chocolaterie@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:02:20 +0100 Subject: [PATCH] Correction partiel TP 5 ++ --- .../example/tpfilrouge/api/RetrofitTools.kt | 4 +- .../com/example/tpfilrouge/article/Article.kt | 2 +- .../tpfilrouge/article/ArticleActivity.kt | 2 +- .../tpfilrouge/article/ArticleFormFragment.kt | 63 +++++++++-- .../tpfilrouge/article/ArticleService.kt | 10 ++ .../tpfilrouge/article/ListArticleFragment.kt | 34 +++++- .../article/ListArticleViewModel.kt | 101 +++++++++++++++--- 7 files changed, 188 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/example/tpfilrouge/api/RetrofitTools.kt b/app/src/main/java/com/example/tpfilrouge/api/RetrofitTools.kt index 32a7851..db59d96 100644 --- a/app/src/main/java/com/example/tpfilrouge/api/RetrofitTools.kt +++ b/app/src/main/java/com/example/tpfilrouge/api/RetrofitTools.kt @@ -11,10 +11,10 @@ class RetrofitTools { companion object { // La racine de l'api - //val BASE_URL = "http://127.0.0.1:3000/" + val BASE_URL = "http://127.0.0.1:3000/" // Pour les personnes emulateurs : - val BASE_URL = "http://10.0.2.2:3000/" + // val BASE_URL = "http://10.0.2.2:3000/" // L'utilitaire conversion JSON <=> Objet val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build(); diff --git a/app/src/main/java/com/example/tpfilrouge/article/Article.kt b/app/src/main/java/com/example/tpfilrouge/article/Article.kt index 3a51c6e..e5b56b0 100644 --- a/app/src/main/java/com/example/tpfilrouge/article/Article.kt +++ b/app/src/main/java/com/example/tpfilrouge/article/Article.kt @@ -1,4 +1,4 @@ package com.example.tpfilrouge.article -data class Article(var title : String, var desc : String, var imgPath : String) { +data class Article(var title : String, var desc : String, var imgPath : String, var id : String = "") { } \ No newline at end of file diff --git a/app/src/main/java/com/example/tpfilrouge/article/ArticleActivity.kt b/app/src/main/java/com/example/tpfilrouge/article/ArticleActivity.kt index b998f05..0b16ce1 100644 --- a/app/src/main/java/com/example/tpfilrouge/article/ArticleActivity.kt +++ b/app/src/main/java/com/example/tpfilrouge/article/ArticleActivity.kt @@ -32,7 +32,7 @@ fun ArticleActivityPage() { startDestination = "list_article" ) { composable("list_article") { ListArticleFragmentPage(viewModel, navController) } - composable("article_form") { ArticleFormFragmentPage(viewModel) } + composable("article_form") { ArticleFormFragmentPage(viewModel, navController) } } } diff --git a/app/src/main/java/com/example/tpfilrouge/article/ArticleFormFragment.kt b/app/src/main/java/com/example/tpfilrouge/article/ArticleFormFragment.kt index 1f764f5..25f42b7 100644 --- a/app/src/main/java/com/example/tpfilrouge/article/ArticleFormFragment.kt +++ b/app/src/main/java/com/example/tpfilrouge/article/ArticleFormFragment.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.navigation.NavController import com.example.tpfilrouge.R import com.example.tpfilrouge.ui.theme.EniButton import com.example.tpfilrouge.ui.theme.EniPage @@ -28,7 +29,7 @@ import com.example.tpfilrouge.ui.theme.EniTextField import kotlinx.coroutines.flow.MutableStateFlow @Composable -fun ArticleForm(viewModel: ListArticleViewModel){ +fun ArticleForm(viewModel: ListArticleViewModel, navController : NavController?=null){ var titleFieldState by remember { mutableStateOf("Un article") } var descFieldState by remember { mutableStateOf("Une description") } @@ -42,25 +43,69 @@ fun ArticleForm(viewModel: ListArticleViewModel){ val newArticle = Article(titleFieldState, descFieldState, "https://avatar.iran.liara.run/public"); // Envoyer l'article via une méthode view model - viewModel.addArticle(newArticle) + viewModel.addArticle(newArticle, onSuccess = { + navController!!.navigate("list_article") + }) } } } @Composable -fun ArticleFormFragmentPage(viewModel: ListArticleViewModel) { +fun ArticleEditForm(viewModel: ListArticleViewModel, navController : NavController?=null){ + var titleFieldState by remember { mutableStateOf(viewModel.editedArticle.title) } + var descFieldState by remember { mutableStateOf(viewModel.editedArticle.desc) } + + Column { + EniTextField(label = "Saisir un Email", value = titleFieldState, + onValueChange = { newValue -> titleFieldState = newValue }) + EniTextField(label = "Saisir une Description", value = descFieldState, + onValueChange = { newValue -> descFieldState = newValue }) + EniButton("Editer") { + // Instancer un article avec les infos de la saisie + val editedArticle = Article(titleFieldState, descFieldState, + "https://avatar.iran.liara.run/public", id = viewModel.editedArticleId); + // Envoyer l'article via une méthode view model + viewModel.editArticle(editedArticle) + } + } +} + +@Composable +fun AddArticleFormPage(viewModel: ListArticleViewModel, navController : NavController?=null){ + Text(text = "Ajouter un article", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = TextStyle(color = Color.White, fontSize = 28.sp) + ) + ArticleForm(viewModel, navController) +} + +@Composable +fun EditArticleFormPage(viewModel: ListArticleViewModel, navController : NavController?=null){ + Text(text = "Modifier un article", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = TextStyle(color = Color.White, fontSize = 28.sp) + ) + ArticleEditForm(viewModel, navController) +} + +@Composable +fun ArticleFormFragmentPage(viewModel: ListArticleViewModel, navController : NavController?=null) { + // Savoir si je suis en mode edition ou ajout + val isEdit by viewModel.isEdit.collectAsState(); EniPage { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(40.dp) ) { - Text(text = "Ajouter un article", - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - style = TextStyle(color = Color.White, fontSize = 28.sp) - ) - ArticleForm(viewModel) + if (isEdit){ + EditArticleFormPage(viewModel, navController) + } + else { + AddArticleFormPage(viewModel, navController) + } } } } diff --git a/app/src/main/java/com/example/tpfilrouge/article/ArticleService.kt b/app/src/main/java/com/example/tpfilrouge/article/ArticleService.kt index 6e86bb8..72dcf2b 100644 --- a/app/src/main/java/com/example/tpfilrouge/article/ArticleService.kt +++ b/app/src/main/java/com/example/tpfilrouge/article/ArticleService.kt @@ -3,13 +3,23 @@ package com.example.demoandroid.demoapi import com.example.tpfilrouge.api.RetrofitTools.Companion.retrofit import com.example.tpfilrouge.api.ServiceResponseDTO import com.example.tpfilrouge.article.Article +import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path interface ArticleService { @GET("articles") suspend fun getArticles() : ServiceResponseDTO> + @POST("articles/save") + suspend fun saveArticle(@Body article : Article) : ServiceResponseDTO
+ + @DELETE("articles/{id}") + suspend fun delete(@Path("id") id :String) : ServiceResponseDTO
+ object ArticleApi { val articleService : ArticleService by lazy { retrofit.create(ArticleService::class.java) } } diff --git a/app/src/main/java/com/example/tpfilrouge/article/ListArticleFragment.kt b/app/src/main/java/com/example/tpfilrouge/article/ListArticleFragment.kt index adefcf3..2cd0774 100644 --- a/app/src/main/java/com/example/tpfilrouge/article/ListArticleFragment.kt +++ b/app/src/main/java/com/example/tpfilrouge/article/ListArticleFragment.kt @@ -1,5 +1,6 @@ package com.example.tpfilrouge.article +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -7,8 +8,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Info import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -32,7 +39,7 @@ import com.example.tpfilrouge.ui.theme.EniPage import kotlinx.coroutines.flow.MutableStateFlow @Composable -fun ArticleCard(article: Article){ +fun ArticleCard(article: Article, viewModel: ListArticleViewModel, navController: NavController?=null){ ElevatedCard( elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), modifier = Modifier.fillMaxWidth().padding(vertical = 14.dp) @@ -48,6 +55,29 @@ fun ArticleCard(article: Article){ Text(article.title, style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 15.sp)) Text(article.desc) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + IconButton(onClick = { + }) { + Icon(imageVector = Icons.Filled.Info, contentDescription = "Voir") + } + IconButton(onClick = { + // Dire au viewmodel qu'on va être en mode édition + viewModel.isEdit.value = true; + // Dire au viewmodel quel article on va modifier + viewModel.editedArticle = article; + // Dire au viewmodel quel Id d'article en cours de proccessus + viewModel.editedArticleId = article.id; + // naviguer dans le formulaire + navController!!.navigate("article_form") + }) { + Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit") + } + IconButton(onClick = { + viewModel.callDeleteArticleApi(article.id); + }) { + Icon(imageVector = Icons.Filled.Delete, contentDescription = "Delete") + } + } } } } @@ -76,7 +106,7 @@ fun ListArticleFragmentPage(viewModel: ListArticleViewModel, navController: NavC } LazyColumn { items(articlesState) { article -> - ArticleCard(article) + ArticleCard(article, viewModel, navController) } } } diff --git a/app/src/main/java/com/example/tpfilrouge/article/ListArticleViewModel.kt b/app/src/main/java/com/example/tpfilrouge/article/ListArticleViewModel.kt index c230654..ca69e61 100644 --- a/app/src/main/java/com/example/tpfilrouge/article/ListArticleViewModel.kt +++ b/app/src/main/java/com/example/tpfilrouge/article/ListArticleViewModel.kt @@ -3,16 +3,63 @@ package com.example.tpfilrouge.article import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.demoandroid.demoapi.ArticleService +import com.example.tpfilrouge.helpers.AlertDialogHelpers import com.example.tpfilrouge.helpers.AppDialogHelpers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch class ListArticleViewModel : ViewModel() { + // Indicateur pour savoir si je modifie ou pas un article + var editedArticleId : String = ""; // Quel est l'article que je modifie + var editedArticle : Article = Article("", "", ""); // Article en cours d'edition + var isEdit = MutableStateFlow(false); // Est-ce que je suis en mode edition ? + var articles = MutableStateFlow>(listOf()); - fun addArticle(article: Article){ - articles.value = articles.value + article; + fun editArticle(article: Article){ + AppDialogHelpers.get().showDialog("Modification de l'article en cours..."); + + viewModelScope.launch { + // Appel api async + val responseService = ArticleService.ArticleApi.articleService.saveArticle(article); + + // Fermer la popup de chargement aprés l'appel de API + AppDialogHelpers.get().closeDialog() + + // Si ajout ok + if (responseService.code.equals("200")){ + AlertDialogHelpers.get().show(responseService.message, onClose = {}) + } + else{ + // Erreur + AlertDialogHelpers.get().show(responseService.message, onClose = {}) + } + } + } + + fun addArticle(article: Article, onSuccess: () -> Unit){ + AppDialogHelpers.get().showDialog("Ajout d'article en cours..."); + + viewModelScope.launch { + // Appel api async + val responseService = ArticleService.ArticleApi.articleService.saveArticle(article); + + // Fermer la popup de chargement aprés l'appel de API + AppDialogHelpers.get().closeDialog() + + // Si ajout ok + if (responseService.code.equals("200")){ + AlertDialogHelpers.get().show(responseService.message, onClose = { + // Naviguer sur la page liste article + onSuccess() + }) + } + else{ + // Erreur + AlertDialogHelpers.get().show(responseService.message, onClose = {}) + } + } } fun callArticlesApi(){ @@ -23,20 +70,48 @@ class ListArticleViewModel : ViewModel() { // Récupérer le metier listArticle via un API Web val serviceResponse = ArticleService.ArticleApi.articleService.getArticles(); - // Plus tard il sera possible de tester le code métier - /* - if (serviceResponse.code.equals("200")) { + // Fermer la popup de chargement aprés l'appel de API + AppDialogHelpers.get().closeDialog() - } - */ - - // La liste des articles ecoutables se met à jour par rapport à la - // la liste des articles dans le data - // Node : on remaque !! aprés data car data est nullable (!! que si nullable)) articles.value = serviceResponse.data!!; - // Fermer la popup de chargement - AppDialogHelpers.get().closeDialog(); + } + } + + fun callDeleteArticleApi(id : String){ + AppDialogHelpers.get().showDialog("Suppression de l'article en cours..."); + + viewModelScope.launch { + // Appel api async + val responseService = ArticleService.ArticleApi.articleService.delete(id); + + // Fermer la popup de chargement aprés l'appel de API + AppDialogHelpers.get().closeDialog() + + // Si suppression ok + if (responseService.code.equals("200")){ + + // Methode 1 : (Le flemmard) Appeler le refresh + // Avantage : Si d'autres user ont supprimés des articles entre temps, on a plus les articles + // supprimés par les autres users à l'écran + // Inconvenients : Moins performant niveau ressource du tel car on on refresh tout l'ecran et en plus on rappel une API + //callArticlesApi(); + + // Methode 2 : Je supprime dans le articles.value l'article je viens de supprimer + // Inconvenients : Si d'autres user ont supprimés des articles entre temps, on a encore les articles + // supprimés par les autres users à l'écran + // Avantage : Plus performant niveau ressource du tel car on fait le strict minimum pour refresh l'ecran + // EXEMPLE : Supprimer un element de la liste directement + val newArticles = articles.value.filterNot { it.id == id } + articles.value = newArticles; + + AlertDialogHelpers.get().show(responseService.message, onClose = { + }) + } + else{ + // Erreur + AlertDialogHelpers.get().show(responseService.message, onClose = {}) + } } }