I am learning coding for Android using Kotlin, Jetpack Compose and AndroidStudio version Hedgehog | 2023.1.1. I use a Windows 11 PC Intel 64 with 8 Gb Ram/512 Gb SSD hd.
One day I decided to make a tuto for myself about using ViewModel, MutableStateFlow, mutableStateOf and a list of objects displayed by an UI. The goal was to be able to update the UI every time I change something on any object (properties which can be modified are: fName, lName). I wanted also to be able to add a new object to the list and remove any of the objects from the list : the displayed list should be updated by these operations also. But I could not get any UI update being done in real time when I do any of the abow changes (on objects or on the list itself).In the ViewModel I can see the change on rhe object, but the UI does not display it. The add operation seems to be not executed and the remove operation updates in the ViewModel (only if I click on any object after the remove operation) but not in the UI. I can not find the issue.
Can you help, please ? Thank you too much in advance for your kindness. PS : The next step is to refactor this app for use of a local database (Room). I guess it should be very hard (Hilt Dependency Injection, Repository, Dao, …). I am afraid.
The whole code is down:
data class Personne(
val id: UUID = UUID.randomUUID(),
val fName: String = "",
val lName: String = "",
)
val liste = listOf(
Personne(id = UUID.randomUUID(),fName = "P1-name", lName = "P1-surname"),
Personne(id = UUID.randomUUID(),fName = "P2-name", lName = "P2-surname"),
Personne(id = UUID.randomUUID(),fName = "P3-name", lName = "P2-surname"),
Personne(id = UUID.randomUUID(),fName = "P4-name", lName = "P2-surname"),
Personne(id = UUID.randomUUID(),fName = "P5-name", lName = "P2-surname"),
Personne(id = UUID.randomUUID(),fName = "P6-name", lName = "P2-surname"),
Personne(id = UUID.randomUUID(),fName = "P7-name", lName = "P2-surname"),
Personne(id = UUID.randomUUID(),fName = "P8-name", lName = "P2-surname"),
)
class MainViewModel : ViewModel() {
private val _personnes = MutableStateFlow(mutableListOf<Personne>())
val personnes = _personnes.asStateFlow()
init {
_personnes.value = liste.toMutableList()
}
fun modifyPrenomPersonne(idPersToModify: String, newPrenom: String) {
var foundedPers = _personnes.value.find { it.id.toString() == idPersToModify }
val index = _personnes.value.indexOf(foundedPers)
if (index != -1) {
viewModelScope.launch(Dispatchers.Default) {
val newList = _personnes.value
foundedPers = foundedPers!!.copy(nom = newPrenom)
newList[index] = foundedPers!!
_personnes.update { newList } // emit the new UI state
//_personnes.value = newList // emit the new UI state
}
}
Log.i("INDEX", "modifyPrenomPersonne: ${_personnes.value}")
}
fun modifyNomPersonne(idPersToModify: String, newNom: String) {
var foundedPers = _personnes.value.find { it.id.toString() == idPersToModify }
val index = if (foundedPers != null) _personnes.value.indexOf(foundedPers) else -1
if (index != -1) {
viewModelScope.launch(Dispatchers.Default) {
val newList = _personnes.value
foundedPers = foundedPers!!.copy(nom = newNom)
newList[index] = foundedPers!!
_personnes.update { newList } // emit the new UI state
//_personnes.value = newList // emit the new UI state
}
}
Log.i("INDEX", "modifyNomPersonne: ${_personnes.value}")
}
fun addPersonne(personneToAdd: Personne) {
val pers = _personnes.value
pers.add(personneToAdd)
_personnes.value = pers
}
fun removePersonne(personneToRemove: Personne) {
val pers = _personnes.value
pers.remove(personneToRemove)
_personnes.value = pers
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TutoViewModelMutableStateFlowByChatGptTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MainPage()
}
}
}
}
}
@Composable
fun MainPage(viewModel: MainViewModel = MainViewModel()) {
val personnes by viewModel.personnes.collectAsState()
var mustEditPersonne = remember { mutableStateOf(false) }
var mustCreatePersonne = remember { mutableStateOf(false) }
var personneToEdit = remember { mutableStateOf(Personne()) }
Scaffold(
topBar = {
CustomAppBar(
title = "TEST DE MutableStateFlow",
icon = null,
showProfile = true,
)
},
floatingActionButton = {
HomeFAB() {
// Click sur FAB = crée une nouvelle personne
mustCreatePersonne.value = true
}
}
) { pdgVal ->
Surface(
modifier = Modifier.padding(pdgVal)
) {
LazyColumn {
items(items = personnes) { personne ->
PersonneCard(
personne = personne,
onCardClick = {
personneToEdit.value = it
mustEditPersonne.value = true
},
onDeleteClick = { personneToDelete ->
viewModel.removePersonne(personneToRemove = personneToDelete)
}
)
}
}
if (mustEditPersonne.value) EditPersonneDialog(
vm = viewModel,
personneToEdit = personneToEdit.value
) {
mustEditPersonne.value = false
println("MainPage after edit - Liste = $personnes")
}
if (mustCreatePersonne.value) CreatePersonneDialog(
vm = viewModel
) {
mustCreatePersonne.value = false
println("MainPage after create new - Liste = $personnes")
}
}
}
}
@Composable
fun EditPersonneDialog(
vm: MainViewModel,
personneToEdit: Personne,
onValidated: () -> Unit
) {
val openDialog = remember { mutableStateOf(value = true) }
val textFName = remember { mutableStateOf(value = personneToEdit.fName) }
val textLName = remember { mutableStateOf(value = personneToEdit.lName) }
val isFNameValide = remember {
mutableStateOf(
value = !(textFName.value.isEmpty() || textFName.value.isBlank()) &&
(textFName.value != personneToEdit.fName)
)
}
val isLNameValide = remember {
mutableStateOf(
value = !(textLName.value.isEmpty() || textLName.value.isBlank()) &&
(textLName.value != personneToEdit.lName)
)
}
if (openDialog.value) {
AlertDialog(
onDismissRequest = {
openDialog.value = false
},
title = {
Text(
text = "Edition des infos",
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
},
text = {
Column {
InputField(
valueState = textFName.value,
labelId = "Modifier le prélName",
placeHolder = "",
enabled = openDialog.value
) { fName ->
textFName.value = fName
}
InputField(
valueState = textLName.value,
labelId = "Modifier le lName",
placeHolder = "",
enabled = openDialog.value
) { lName ->
textLName.value = lName
}
}
},
confirmButton = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
RoundedButton(
label = "OK"
) {
if (isFNameValide.value) {
vm.modifyFNamePersonne(
idPersToModify = personneToEdit.id.toString(),
newFName = textFName.value
)
}
if (isLNameValide.value) {
vm.modifyLNamePersonne(
idPersToModify = personneToEdit.id.toString(),
newLName = textLName.value
)
}
println("Validation - FName = ${textFName.value} / LName = ${textLName.value}")
println("Validation - Liste = ${vm.personnes.value}")
openDialog.value = false
onValidated()
}
}
}
)
}
}
@Composable
fun CreatePersonneDialog(
vm: MainViewModel,
onCreated: () -> Unit // Fonction définie dans l'Ui appelante
) {
val openDialog = remember { mutableStateOf(value = true) }
val textFName = remember { mutableStateOf(value = "") }
val textLName = remember { mutableStateOf(value = "") }
val isInputValide = remember {
mutableStateOf(
value = !(textFName.value.isBlank() || textFName.value.isEmpty()) &&
!(textLName.value.isBlank() || textLName.value.isEmpty())
)
}
if (openDialog.value) {
AlertDialog(
onDismissRequest = {
openDialog.value = false
},
title = {
Text(
text = "Nouvelle personne",
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
},
text = {
Column {
InputField(
valueState = textFName.value,
labelId = "Indiquez le prélName",
placeHolder = "",
enabled = openDialog.value
) { fName ->
textFName.value = fName
println("FName: $textFName")
}
InputField(
valueState = textLName.value,
labelId = "Indiquez le lName",
placeHolder = "",
enabled = openDialog.value
) { lName ->
textLName.value = lName
println("LName: $textLName")
}
}
},
confirmButton = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
RoundedButton(
label = "OK"
) {
if (isInputValide.value) {
val newPersonne =
Personne(
fName = textFName.value,
lName = textLName.value
)
Log.i("INDEX", "CreatePersonneDialog: $newPersonne")
vm.addPersonne(personneToAdd = newPersonne)
println("Validation - Liste = ${vm.personnes.value}")
}
openDialog.value = false
onCreated()
}
}
}
)
}
}
@Composable
fun InputField(
modifier: Modifier = Modifier,
valueState: String,
labelId: String = "Edition",
placeHolder: String,
enabled: Boolean,
isSingleLine: Boolean = true,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Next,
onAction: KeyboardActions = KeyboardActions.Default,
onValueChanged: (String) -> Unit,
) {
var inputText by remember { mutableStateOf(valueState) }
OutlinedTextField(
value = inputText,
onValueChange = {
inputText = it
onValueChanged(it)
},
label = { Text(text = labelId) },
placeholder = { Text(text = placeHolder) },
singleLine = isSingleLine,
textStyle = TextStyle(
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onBackground
),
modifier = Modifier
.padding(bottom = 10.dp, start = 10.dp, end = 10.dp)
.fillMaxWidth(),
enabled = enabled,
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
imeAction = imeAction
),
keyboardActions = onAction
)
}
@Composable
fun RoundedButton(
label: String = "Reading",
radius: Int = 29,
onPress: () -> Unit = {}
) {
Surface(
modifier = Modifier.clip(
RoundedCornerShape(
bottomEndPercent = radius,
topStartPercent = radius
)
),
color = Color(0xff92cbdf)
) {
Column(
modifier = Modifier
.width(90.dp)
.heightIn(40.dp)
.clickable {
onPress.invoke()
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = label,
modifier = Modifier.padding(2.dp),
style = TextStyle(
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
)
}
}
}
// Gradle(Project Level) :
plugins {
id("com.android.application") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
}
// Gradle(Module Level) :
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.tutoviewmodelmutablestateflowbychatgpt"
compileSdk = 34
defaultConfig {
applicationId = "com.example.tutoviewmodelmutablestateflowbychatgpt"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildToolsVersion = "34.0.0"
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.8.1")
implementation(platform("androidx.compose:compose-bom:2023.10.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
implementation("androidx.compose.material:material-icons-extended")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}