Search code examples
androidkotlinandroid-roomkotlin-stateflow

How to hide or NOT SHOW items in a Room Database based on a Boolean


I have a Todo Application and I want to hide (which basically means not showing)the tasks based on its completed status(strikeThrough over the text). However, the hideCompleted tasks implementation I followed isn't working but the sort and search is working and I said this because I put all the Implementations in a single query and made them work together with stateFlow but the hide isn't working. Here is my code. Okay What I mean by isn't working is that it unchecks the checkBoxes besides the Tasks instead of hiding them.

First My Model class

import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.*

/** Our Model class. This class will represent our database table **/


@Entity(tableName = "todo_table")
data class Todo(
    @PrimaryKey (autoGenerate = true) // here "Room" will autoGenerate the id for us 
instead of assigning a randomUUID value
    val id : Int = 0,
    var title : String = "",
    var date : Date = Date(),
    var time : Date = Date(),
    var todoCheckBox : Boolean = false
)

Then my Dao. Only the two sort(By date and by Name) functions are directly accessed from the Dao. The others are through the repository.

    import androidx.room.*
    import com.bignerdranch.android.to_dolist.model.Todo
    import kotlinx.coroutines.flow.Flow

    /**
     *  This will be our DAO file where we will be update, delete and add Todos to our 
    database so it contains the methods used for accessing the database
 */

@Dao
interface TodoDao {

    // function to hold all out queries and will be executed based on our sortOrder
    fun getAllTasks(query : String, sortOrder: SortOrder, hideCompleted: Boolean) : Flow<List<Todo>> =
        when(sortOrder) {
            SortOrder.BY_DATE -> getTasksSortedByDateCreated(query, hideCompleted)
            SortOrder.BY_NAME -> getTasksSortedByName(query, hideCompleted)
        }
    

    @Query("SELECT * FROM todo_table WHERE (todoCheckBox != :hideCompleted OR todoCheckBox = 0) AND title LIKE '%' || :searchQueryText || '%'  ORDER BY title COLLATE NOCASE")
    fun getTasksSortedByName(searchQueryText : String, hideCompleted : Boolean): Flow<List<Todo>>

    @Query("SELECT * FROM todo_table WHERE (todoCheckBox != :hideCompleted OR todoCheckBox = 0) AND title LIKE '%' || :searchQueryText || '%'  ORDER BY time ASC")
    fun getTasksSortedByDateCreated(searchQueryText : String, hideCompleted : Boolean): Flow<List<Todo>>

    // onConflict will ignore any known conflicts, in this case will remove duplicate "Todos" with the same name
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun addTodo(todo: Todo)

    @Query("DELETE FROM todo_table WHERE id IN (:idList)")
    suspend fun deleteSelectedTasks(idList : Long)

    @Query("DELETE FROM todo_table")
    suspend fun deleteAllTasks()

}

My ViewModel(Where I call the sort functions directly from the Dao)

import android.app.Application
import androidx.lifecycle.*
import com.bignerdranch.android.to_dolist.model.Todo
import com.bignerdranch.android.to_dolist.repository.TodoRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch

/** Our AndroidViewModel. This AndroidViewModel holds reference to our Application context. **/

class TodoViewModel(application: Application) : AndroidViewModel(application) {

    /**
     *  NOTE! : "Context" are needed to instantiate a database that is why we are using 
an AndroidViewModel in this case because it holds reference to an
     *  Application context. And if I remember correctly, it will start as the "Application" starts.
 **/

    private val repository : TodoRepository
    private val userDao = TodoDatabase.getDatabase(application).todoDao()

    init {
        // having access to our TodoDao from our database
        val userDao = TodoDatabase.getDatabase(application).todoDao()
        repository = TodoRepository(userDao)
    }


    val searchQuery = MutableStateFlow("")
    val sortOrder = MutableStateFlow(SortOrder.BY_DATE) // adding BY_DATE to make the 
lists sorted by date as default
    val hideCompleted = MutableStateFlow(false)


    /**
     *  The combine function here is a an object in the flow library that is used too 
combine the most recent values of a flow, so if one value changes it will
     *  automatically return the latest values of the other flows. This is done so that the three flows will work in harmony.
     */
    private val tasksFlow = combine(
        searchQuery,
        sortOrder,
        hideCompleted
    ) { query, sortOrder, hideCompleted -> // LAMBDA
        Triple(query, sortOrder, hideCompleted)
        // flatMapLatest gets triggered when any of this flows changes and then passes it to the query to be executed.
    }.flatMapLatest { (query, sortOrder, hideCompleted) ->
        userDao.getAllTasks(query, sortOrder, hideCompleted)

    }

    val tasks = tasksFlow.asLiveData()


    // All functions using coroutines objects indicates that whatever is in it should run in a background thread
    fun addTodo(todo : Todo) {
        viewModelScope.launch(Dispatchers.IO) {
            repository.addTodo(todo)
        }
    }

    fun deleteSelectedTasks(idList: Long) {
        viewModelScope.launch(Dispatchers.IO) {
            repository.delSelectedTasks(idList)
        }
    }

    fun deleteAllTasks() {
        viewModelScope.launch(Dispatchers.IO) {
            repository.delAllTasks()
        }
    }
}

enum class SortOrder { BY_DATE, BY_NAME }

Then my Fragment

import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.Toast 
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bignerdranch.android.to_dolist.databinding.FragmentListBinding
import com.bignerdranch.android.to_dolist.R
import com.bignerdranch.android.to_dolist.data.SortOrder
import com.bignerdranch.android.to_dolist.data.TodoViewModel
import com.bignerdranch.android.to_dolist.model.Todo
import com.bignerdranch.android.to_dolist.utils.onQueryTextChanged

private const val TAG = "ListFragment"

class ListFragment : Fragment() {
    private var _binding : FragmentListBinding? = null
    private val binding get() = _binding!!
    lateinit var mTodoViewModel: TodoViewModel
    private lateinit var recyclerView: RecyclerView
    private val adapter = ListAdapter()  // getting reference to our ListAdapter
    private var todosList = emptyList<Todo>()


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout for this fragment with ViewBinding style
        _binding = FragmentListBinding.inflate(inflater, container, false)

        // this tells our activity/fragment that we have a menu_item it should respond to it.
        setHasOptionsMenu(true)

        recyclerView = binding.recyclerViewTodo
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(requireContext())


        /**
         *  updates our recyclerView with the new "observed" changes in our database through our adapter
         */
        // TodoViewModel
        mTodoViewModel = ViewModelProvider(this)[TodoViewModel::class.java]
        mTodoViewModel.tasks.observe(viewLifecycleOwner) { todos ->
            adapter.setData(todos)
            todosList = todos
        }

        // Add Task Button
        binding.fbAdd.setOnClickListener {
            findNavController().navigate(R.id.action_listFragment_to_addFragment)
        }
        return binding.root
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.fragment_list, menu)

        val search = menu.findItem(R.id.todo_search)
        val searchView = search.actionView as SearchView

        searchView.onQueryTextChanged { querySearch ->
            mTodoViewModel.searchQuery.value = querySearch
        }

    }


    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when(item.itemId)  {
            R.id.sort_by_name -> {
                mTodoViewModel.sortOrder.value = SortOrder.BY_NAME
                true
            }

            R.id.sort_by_date -> {
                mTodoViewModel.sortOrder.value = SortOrder.BY_DATE
                true
            }

            R.id.todo_hide_completed -> {
                item.isChecked = !item.isChecked
                mTodoViewModel.hideCompleted.value = item.isChecked
                true
            }

            R.id.del_selected_tasks -> {
                deleteSelectedUsers()
                 true
            }

            R.id.del_all_tasks -> {
                deleteAllTasks()
                true
            }

            else -> super.onOptionsItemSelected(item)
        }
    }

    // function to delete all of our Tasks
    private fun deleteAllTasks() {
        val builder = AlertDialog.Builder(requireContext())
        builder.setPositiveButton("Yes") {_,_->
            mTodoViewModel.deleteAllTasks()
            Toast.makeText(requireContext(), "All tasks have been successfully deleted!", Toast.LENGTH_LONG).show()
        }
        builder.setNegativeButton("No") {_,_-> }
        builder.setTitle("Confirm Deletion")
        builder.setMessage("Are you sure you want to delete all Tasks?")
        builder.create().show()
    }

    // function to delete only selected Tasks
    @SuppressLint("NotifyDataSetChanged")
    private fun deleteSelectedUsers() {
        val builder = AlertDialog.Builder(requireContext())
        // Our todos that have been marked completed by the checkBox
        val finishedTodos = todosList.filter { it.todoCheckBox }

        builder.setPositiveButton("Yes") {_,_->
            finishedTodos.forEach { todos ->
                mTodoViewModel.deleteSelectedTasks(todos.id.toLong())
            }
            Toast.makeText(requireContext(), "Selected tasks successfully deleted!", Toast.LENGTH_LONG).show()
        }
        builder.setNegativeButton("No") {_,_-> }
        builder.setTitle("Confirm Deletion")
        builder.setMessage("Are you sure you want to delete only selected Tasks?")
        builder.create().show()
        Log.i(TAG , "Our todos list size is ${finishedTodos.size}")
    }  


    // We want to leave no trace of our Binding class Reference to avoid memory leaks
    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}

Solution

  • I was able to find a solution. It turns out there was no logic to actually change the boolean value of the todoCheckBox(was changed to completed), it was just adding a strikeThrough. So I followed a better method to Implement the strikeThrough and refactored some of the code. So here's my code.

    My Adapter

    import android.view.LayoutInflater
    import android.view.ViewGroup
    import androidx.recyclerview.widget.DiffUtil
    import androidx.recyclerview.widget.RecyclerView
    import androidx.recyclerview.widget.ListAdapter
    import com.bignerdranch.android.to_dolist.databinding.CustomRowBinding
    import com.bignerdranch.android.to_dolist.fragments.add.SIMPLE_DATE_FORMAT
    import com.bignerdranch.android.to_dolist.fragments.add.SIMPLE_TIME_FORMAT
    import com.bignerdranch.android.to_dolist.model.Todo
    import java.text.SimpleDateFormat
    import java.util.*
    
    
    class TodoAdapter(private val listener : OnItemClickListener): 
    ListAdapter<Todo, TodoAdapter.TodoViewHolder>(DiffCallBack) {
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
            // this can be done in an inline variable and I may experiment on it later.
            val binding = CustomRowBinding.inflate(LayoutInflater.from(parent.context),
                parent,
                false
            )
            return TodoViewHolder(binding)
        }
    
        override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
            val currentItem = getItem(position)
            holder.bind(currentItem)
        }
    
        inner class TodoViewHolder(private val binding : CustomRowBinding) : RecyclerView.ViewHolder(binding.root) {
    
    
            /** Calling onClickListeners for each _Todo and the associated checkBox. **/
    
            init {
                binding.apply {
                    root.setOnClickListener {
                        val position = adapterPosition // this represents the position of any item in the root layout
                        // NO_POSITION means that an item is invalid and out of this list, so this is a safe check because-
                        // we don't want to call a listener on an invalid item
                        if (position != RecyclerView.NO_POSITION) {
                            val todo = getItem(position)
                            listener.onItemClick(todo)
                        }
                    }
                    cbTask.setOnClickListener {
                        val position = adapterPosition
                        if (position != RecyclerView.NO_POSITION) {
                            val todo = getItem(position)
                            listener.onCheckBoxClick(todo, cbTask.isChecked)
                        }
                    }
                }
            }
    
            fun bind(todo : Todo) {
                val dateLocales = SimpleDateFormat(SIMPLE_DATE_FORMAT, Locale.getDefault())
                val timeLocales = SimpleDateFormat(SIMPLE_TIME_FORMAT, Locale.getDefault())
                binding.apply {
                    tvTaskTitle.text = todo.title
                    tvTaskDate.text = dateLocales.format(todo.date)
                    tvTaskTime.text = timeLocales.format(todo.time)
                    cbTask.isChecked = todo.completed
                    tvTaskTitle.paint.isStrikeThruText = todo.completed
                }
            }
        }
    
        interface OnItemClickListener {
            fun onItemClick(todo : Todo)
            fun onCheckBoxClick(todo: Todo, isChecked: Boolean)
        }
    
        // This piece of code checks between our old and changed and lists and updates the recyclerView with the latest list.
        // This also stops the recyclerView from redrawing itself after the position of an item has been changed. It even provides a nice animation.
        object DiffCallBack : DiffUtil.ItemCallback<Todo>() {
            override fun areItemsTheSame(oldItem: Todo, newItem: Todo) =
                oldItem.id == newItem.id
    
            override fun areContentsTheSame(oldItem: Todo, newItem: Todo) =
                oldItem == newItem
        }
    }
    

    Fragment

    import android.annotation.SuppressLint
    import android.app.AlertDialog
    import android.os.Bundle
    import android.util.Log
    import android.view.*
    import android.widget.Toast
    import androidx.appcompat.widget.SearchView
    import androidx.fragment.app.Fragment
    import androidx.lifecycle.ViewModelProvider
    import androidx.navigation.fragment.findNavController
    import androidx.recyclerview.widget.LinearLayoutManager
    import androidx.recyclerview.widget.RecyclerView
    import com.bignerdranch.android.to_dolist.databinding.FragmentListBinding
    import com.bignerdranch.android.to_dolist.R
    import com.bignerdranch.android.to_dolist.viewmodel.SortOrder
    import com.bignerdranch.android.to_dolist.viewmodel.TodoViewModel
    import com.bignerdranch.android.to_dolist.model.Todo
    import com.bignerdranch.android.to_dolist.utils.onQueryTextChanged
    
    private const val TAG = "ListFragment"
    
    class ListFragment : Fragment(), TodoAdapter.OnItemClickListener {
        private var _binding : FragmentListBinding? = null
        private val binding get() = _binding!!
        private lateinit var mTodoViewModel: TodoViewModel
        private lateinit var recyclerView: RecyclerView
        private val adapter = TodoAdapter(this)
        private var todosList = emptyList<Todo>()
    
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View {
            // Inflate the layout for this fragment with ViewBinding style
            _binding = FragmentListBinding.inflate(inflater, container, false)
    
            // this tells our activity/fragment that we have a menu_item it should respond to it.
            setHasOptionsMenu(true)
    
            recyclerView = binding.recyclerViewTodo
            recyclerView.adapter = adapter
            recyclerView.layoutManager = LinearLayoutManager(requireContext())
            recyclerView.setHasFixedSize(true)
    
            /**
             *  updates our recyclerView with the new "observed" changes in our database through our adapter
             */
            // TodoViewModel
            mTodoViewModel = ViewModelProvider(this)[TodoViewModel::class.java]
            mTodoViewModel.tasks.observe(viewLifecycleOwner) { todos ->
                adapter.submitList(todos)
                todosList = todos
            }
    
            // Add Task Button
            binding.fbAdd.setOnClickListener {
            
    findNavController().navigate(R.id.action_listFragment_to_addFragment)
            }
            return binding.root
        }
    
        override fun onItemClick(todo: Todo) {
            mTodoViewModel.onTaskSelected(todo)
        }
    
        override fun onCheckBoxClick(todo: Todo, isChecked: Boolean) {
            mTodoViewModel.onTaskCheckedChanged(todo, isChecked)
        }
    
        override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
            inflater.inflate(R.menu.fragment_list, menu)
    
            val search = menu.findItem(R.id.todo_search)
            val searchView = search.actionView as SearchView
    
            searchView.onQueryTextChanged { querySearch ->
                mTodoViewModel.searchQuery.value = querySearch
            }
        }
    
    
        override fun onOptionsItemSelected(item: MenuItem): Boolean {
            return when(item.itemId)  {
                R.id.sort_by_name -> {
                    mTodoViewModel.sortOrder.value = SortOrder.BY_NAME
                    true
                }
                R.id.sort_by_date -> {
                    mTodoViewModel.sortOrder.value = SortOrder.BY_DATE
                    true
                }
                R.id.action_hide_completed_tasks -> {
                    item.isChecked = !item.isChecked
                    mTodoViewModel.hideCompleted.value = item.isChecked
                    true
                }
                R.id.del_selected_tasks -> {
                    deleteSelectedUsers()
                     true
                }
                R.id.del_all_tasks -> {
                    deleteAllTasks()
                    true
                }
                else -> super.onOptionsItemSelected(item)
            }
        }
    
        // function to delete all of our Tasks
        private fun deleteAllTasks() {
            val builder = AlertDialog.Builder(requireContext())
            builder.setPositiveButton("Yes") {_,_->
                mTodoViewModel.deleteAllTasks()
                Toast.makeText(requireContext(), "All tasks have been successfully deleted!", Toast.LENGTH_LONG).show()
            }
            builder.setNegativeButton("No") {_,_-> }
            builder.setTitle("Confirm Deletion")
            builder.setMessage("Are you sure you want to delete all Tasks?")
            builder.create().show()
        }
    
        // function to delete only selected Tasks
        @SuppressLint("NotifyDataSetChanged")
        private fun deleteSelectedUsers() {
            val builder = AlertDialog.Builder(requireContext())
            // Our todos that have been marked completed by the checkBox
            val finishedTodos = todosList.filter { it.completed }
    
            builder.setPositiveButton("Yes") {_,_->
                finishedTodos.forEach { todos ->
                    mTodoViewModel.deleteSelectedTasks(todos.id.toLong())
                }
                Toast.makeText(requireContext(), "Selected tasks successfully deleted!", Toast.LENGTH_LONG).show()
            }
            builder.setNegativeButton("No") {_,_-> }
            builder.setTitle("Confirm Deletion")
            builder.setMessage("Are you sure you want to delete only selected Tasks?")
            builder.create().show()
            Log.i(TAG , "Our todos list size is ${finishedTodos.size}")
        }
    
    
        // We want to leave no trace of our Binding class Reference to avoid memory leaks
        override fun onDestroy() {
            super.onDestroy()
            _binding = null
        }
    }
    

    And then just add both functions in the ViewModel

    fun onTaskSelected(task : Todo) {
        TODO()
    }
    
    fun onTaskCheckedChanged(todo : Todo, isChecked : Boolean) {
        viewModelScope.launch {
            repository.updateTask(todo.copy(completed = isChecked))
        }
    }