I am currently working on a small project to understand the functionality and implementation of Room. So far, I have made good progress.
However, I am facing an issue with the checkbox state. Whenever I click on it, the state of all the checkboxes in all the added notes change. I believe this is due to a problem with general and local state, but I am unable to find a solution on my own.
I hope I have explained my problem clearly.
Here are the different parts of my code:
@Entity("notes")
data class Note(
val task: String,
val isChecked: Boolean,
@PrimaryKey (autoGenerate = true)
val id: Int = 0
)
data class NoteState(
val noteId: Int = 0,
val notes: List<Note> = emptyList(),
val task: String = "",
val isAddingTask: Boolean = false,
val isNotChecked: Boolean = false
)
sealed interface NoteEvents {
object SaveNotes: NoteEvents
data class SetTask(val task: String) : NoteEvents
object ShowDialog: NoteEvents
object HideDialog: NoteEvents
data class IsCheckNote(val noteId: Int): NoteEvents
data class DeleteNote(val note: Note) : NoteEvents
}
class NoteViewModel(
private val dao: NoteDao
) : ViewModel() {
private val _notes = dao.getNotes()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
private val _state = MutableStateFlow(NoteState())
val state = combine(_state, _notes) { state, notes ->
state.copy(
notes = notes
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), NoteState())
fun onEvent(event: NoteEvents) {
when(event) {
NoteEvents.SaveNotes -> {
val task = state.value.task
val isChecked = state.value.isNotChecked
if (task.isBlank()) return
val note = Note(
task = task,
isChecked = isChecked
)
viewModelScope.launch {
dao.upsertNote(note)
}
_state.update {
it.copy(
isAddingTask = false,
task = "",
isNotChecked = false
)
}
}
is NoteEvents.SetTask -> {
_state.update {
it.copy(task = event.task)
}
}
is NoteEvents.DeleteNote -> {
viewModelScope.launch {
dao.deleteNote(note = event.note)
}
}
is NoteEvents.IsCheckNote -> {
_state.update {
it.copy(isNotChecked = !it.isNotChecked)
}
}
NoteEvents.ShowDialog -> {
_state.update {
it.copy(isAddingTask = true)
}
}
NoteEvents.HideDialog -> {
_state.update {
it.copy(isAddingTask = false)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NoteScreen(
state: NoteState,
onEvent: (NoteEvents) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = "EYE NOTE",
fontWeight = FontWeight.ExtraBold,
textAlign = TextAlign.Center
)
}
)
},
floatingActionButton = {
FloatingActionButton(onClick = {
onEvent(NoteEvents.ShowDialog)
}) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add note"
)
}
},
) { paddingValues ->
if(state.isAddingTask) {
AddNoteDialog(state = state, onEvent = onEvent)
}
LazyColumn(
contentPadding = paddingValues,
modifier = Modifier
.fillMaxSize()
.padding(start = 20.dp, end = 20.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
items(state.notes) { note ->
NoteBox(
task = note.task,
delete = {
onEvent(NoteEvents.DeleteNote(note))
},
isChecked = state.isNotChecked,
onCheckChange = {
onEvent(NoteEvents.IsCheckNote(note.id))
}
)
}
}
}
}
@Composable
fun NoteBox(
task: String,
isChecked: Boolean,
onCheckChange: (Boolean) -> Unit,
delete: () -> Unit,
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier
.fillMaxWidth(),
shadowElevation = 5.dp,
shape = RoundedCornerShape(20)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.primaryContainer
)
.padding(start = 5.dp)
) {
Checkbox(
checked = isChecked,
onCheckedChange = onCheckChange
)
Text(
text = task,
fontSize = 14.sp,
modifier = modifier.weight(1f)
)
IconButton(
onClick = delete
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete note"
)
}
}
}
}
PS: I tried to tinker with the Note ID without success.
You are passing an id
of the note to the event isCheckNote
and yet you are not using it anywhere in the when
block of viewModel
.
In order to do this all you have to do is whenever you click on a checkbox
you have to insert a new note to the database but with the same id, and configure you conflict startegy to automatically replace any inserted note with same id.
so you dao has to be something like:
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertNote(note : Note)
Then in your viewModel your isCheckNote event(I don't like this name i would personally name it onNoteCheck or onCheckNote) should be like:
is NoteEvents.IsCheckNote -> {
viewModelScope.launch{noteId->
//I'm assuming you have your notes in list you can edit this as it suits you.
val noteToUpdate = notes.find{note->
note.id == noteId
}
noteToUpdate?.let{note->
dao.insertNote(
//this returns the note that you checked but with the isChecked changed
note.copy(isChecked = !note.isChecked)
)
}
}
}
Here is a repository for a similar app by philipp Lackner but with more advanced best practices(MVVM, koltin Channels...), and he made a video about it.
Note: this not tested if it works because i don't have the whole project but the logic remains the same.