Issue
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))
}
}
Answered By - Daniel Iroka
Answer Checked By - Pedro (JavaFixing Volunteer)