Search code examples
kotlinandroid-jetpack-composestateandroid-room

All my components share a similar state and I would like them to have their own state


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.


Solution

  • 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.