Issue
I am working on a TodoList App and I set an Implementation to NOT allow users to add a Task if one or all of the required fields are empty. This Implementation was working perfectly before. However, when I recently encountered some bugs, it mysteriously stopped working. I've rechecked everything and even the commit when I made it work the first time and I didn't see any difference but it still isn't working. I humbly ask that you check to see if my implementation is right and suggest how to properly handle this feature. Here is my code...
First my Model
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
)
AddFragment
import android.graphics.Color
import android.os.Bundle
import android.text.SpannableString
import android.text.TextPaint
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import com.bignerdranch.android.to_dolist.R
import com.bignerdranch.android.to_dolist.data.TodoViewModel
import com.bignerdranch.android.to_dolist.databinding.FragmentAddBinding
import com.bignerdranch.android.to_dolist.fragments.dialogs.DatePickerFragment
import com.bignerdranch.android.to_dolist.fragments.dialogs.TimePickerFragment
import com.bignerdranch.android.to_dolist.model.Todo
import java.text.SimpleDateFormat
import java.util.*
private const val DIALOG_DATE = "DialogDate"
private const val DIALOG_TIME = "DialogTime"
const val SIMPLE_DATE_FORMAT = "MMM, d yyyy"
const val SIMPLE_TIME_FORMAT = "H:mm"
private const val TAG = "AddFragment"
class AddFragment : Fragment() {
private lateinit var todoViewModel : TodoViewModel
private var _binding : FragmentAddBinding? = null
private val binding get() = _binding!!
private lateinit var todo : Todo
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
todo = Todo()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAddBinding.inflate(inflater, container, false)
todoViewModel = ViewModelProvider(this)[TodoViewModel::class.java]
remindersTextSpan()
// will insert our database when clicked
binding.abCheckYes.setOnClickListener {
insertTodoInDatabase()
}
// showing our datePickerDialog
binding.edDate.setOnClickListener {
childFragmentManager.setFragmentResultListener("requestKey", viewLifecycleOwner)
{_, bundle ->
val result = bundle.getSerializable("bundleKey") as Date
// passing the result of the user selected date directly to the _Todo class instead
todo.date = result
// will ONLY update the date field when it is not empty so that we don't get a preloaded Date textView
if(todo.date.toString().isNotEmpty()) {
updateDate()
}
}
DatePickerFragment().show([email protected], DIALOG_DATE)
}
// showing our timePickerDialog
binding.edTime.setOnClickListener {
childFragmentManager.setFragmentResultListener("tRequestKey",
viewLifecycleOwner) {_, bundle ->
val result = bundle.getSerializable("tBundleKey") as Date
// passing the result of the user selected time directly to the _Todo class instead
todo.time = result
// will ONLY update the time field when it is not empty so that we don't get a preloaded Time textView
if (todo.time.toString().isNotEmpty()) {
updateTime()
}
}
TimePickerFragment().show([email protected], DIALOG_TIME)
}
return binding.root
}
// function to update Date
private fun updateDate() {
val dateLocales = SimpleDateFormat(SIMPLE_DATE_FORMAT, Locale.getDefault())
binding.edDate.text = dateLocales.format(todo.date)
}
// function to update Time
private fun updateTime() {
val timeLocales = SimpleDateFormat(SIMPLE_TIME_FORMAT, Locale.getDefault())
binding.edTime.text = timeLocales.format(todo.time)
}
// our reminders Text span
private fun remindersTextSpan() {
val spannableString = SpannableString("Set Reminders")
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
Toast.makeText(context, "Set Reminders!", Toast.LENGTH_LONG).show()
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.color = Color.BLUE
}
}
spannableString.setSpan(clickableSpan, 0, 13,
SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
binding.tvReminders.text = spannableString
binding.tvReminders.movementMethod = LinkMovementMethod.getInstance()
}
// This function's job will be to insert Tasks in our database
private fun insertTodoInDatabase() {
val title = binding.edTaskTitle.text.toString()
val date = binding.edDate.text.toString()
val time = binding.edTime.text.toString()
if (inputCheck(title, date, time)) {
val todo = Todo(0, title)
todoViewModel.addTodo(todo)
// This will make a toast saying Successfully added task if we add a task
Toast.makeText(requireContext(), R.string.task_add_toast,
Toast.LENGTH_LONG).show()
// FIXME: There is a bug here that allows the user to add Todos even when some OR all of the fields are empty.
// FIXME: It has to do with updateTime and updateDate functions I think, so I will try to fix it but maybe not now.
findNavController().navigate(R.id.action_addFragment_to_listFragment)
} else {
Toast.makeText(requireContext(), R.string.no_task_add_toast,
Toast.LENGTH_LONG).show()
}
Log.d(TAG, "Our todo widgets are $title $date and $time")
}
// This function will help us check if the texts are empty and then proceed to add them to the database
// so that we do not add empty tasks to our database
private fun inputCheck(title : String, date: String, time: String) : Boolean {
// will return false if fields in TextUtils are empty and true if not
return !(TextUtils.isEmpty(title) && TextUtils.isEmpty(date) && time.isEmpty())
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
My DAO
/**
* 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 {
// 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("SELECT * FROM todo_table ORDER BY id ASC")
fun readAllData() : LiveData<List<Todo>>
@Query("DELETE FROM todo_table WHERE id IN (:idList)")
suspend fun deleteSelectedTasks(idList : Long)
@Query("DELETE FROM todo_table")
suspend fun deleteAllTasks()
}
These are the emptyLists which weren't supposed be added even if one is missing which is the title in this case.
Then this my debugging test which proves that some or all the fields in any attempted Task is actually empty. The debug log messages that have spaces between "and" or only a date or a time proves that some or all the fields are empty but it still gets added. This was working perfectly before, I don't know why it stopped.
Solution
I think so... An error occurred in the inputCheck method.
Change the and operator to the or operator.
before
// This function will help us check if the texts are empty and then proceed to add them to the database
// so that we do not add empty tasks to our database
private fun inputCheck(title : String, date: String, time: String) : Boolean {
// will return false if fields in TextUtils are empty and true if not
return !(TextUtils.isEmpty(title) && TextUtils.isEmpty(date) && time.isEmpty())
}
after
// This function will help us check if the texts are empty and then proceed to add them to the database
// so that we do not add empty tasks to our database
private fun inputCheck(title : String, date: String, time: String) : Boolean {
// will return false if fields in TextUtils are empty and true if not
return !(TextUtils.isEmpty(title) || TextUtils.isEmpty(date) || time.isEmpty())
}
add comment.
'and' operator is all values must be true.
'is' operator is true even if only one value is true.
for example, field state. (title is empty, date is not empty, time is not empty.)
Before method step.
- !(TextUtils.isEmpty(title) && TextUtils.isEmpty(date) && time.isEmpty())
- !(true && false && false)
- !(false)
- true
After method step.
- !(TextUtils.isEmpty(title) || TextUtils.isEmpty(date) || time.isEmpty())
- !(true || false || false)
- !(true)
- false
Answered By - Seungho Kang
Answer Checked By - Candace Johnson (JavaFixing Volunteer)