Hello! I found this challenge pretty straightforward except for all the messing around I had to do with date and time formatting… but I think I cracked it. Would love any thoughts/improvements!
First of all, following the book exactly, I noticed that whenever a new date is chosen with DatePickerFragment, it would indeed send a new year, month, and day back to CrimeFragment, BUT reset the time to a default value (12:00 AM).
I addressed it by adding relevant hour and minute values to GregorianCalendar that were missing in the original (DatePickerFragment.kt):
...
private const val ARG_DATE = "date"
class DatePickerFragment : DialogFragment() {
interface Callbacks {
fun onDateSelected(date:Date)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val date = arguments?.getSerializable(ARG_DATE) as Date
val calendar = getInstance()
calendar.time = date
val calendarHour = calendar.get(HOUR_OF_DAY)
val calendarMinute = calendar.get(MINUTE)
val dateListener = DatePickerDialog.OnDateSetListener {
_: DatePicker, year: Int, month: Int, day: Int ->
val resultDate : Date =
GregorianCalendar(year, month, day, calendarHour, calendarMinute).time
targetFragment?.let { fragment ->
(fragment as Callbacks).onDateSelected(resultDate)
}
}
val initialYear = calendar.get(YEAR)
val initialMonth = calendar.get(MONTH)
val initialDay = calendar.get(DAY_OF_MONTH)
return DatePickerDialog(
requireContext(),
dateListener,
initialYear,
initialMonth,
initialDay
)
}
...
}
As for the challenge itself, I basically duplicated DatePickerFragment, adjusting as necessary to reflect the new TimePickerFragment, and of course adding a new button to fragment_crime.xml. Both DatePickerFragment and TimePickerFragment adjust the same Date object (the former changes year, month, and day; the latter hours and minutes). I also changed some of the Date formatting in CrimeFragment to make it a little neater.
TimePickerFragment.kt:
package com.bignerdranch.android.criminalintent
import android.app.Dialog
import android.app.TimePickerDialog
import android.os.Bundle
import android.widget.TimePicker
import androidx.fragment.app.DialogFragment
import java.util.*
import java.util.Calendar.*
private const val ARG_TIME = "time"
class TimePickerFragment: DialogFragment() {
interface Callbacks {
fun onTimeSelected(date: Date)
}
override fun onCreateDialog(SavedInstanceState: Bundle?): Dialog {
val date = arguments?.getSerializable(ARG_TIME) as Date
val calendar = getInstance()
calendar.time = date
val calendarYear = calendar.get(YEAR)
val calendarMonth = calendar.get(MONTH)
val calendarDay = calendar.get(DAY_OF_MONTH)
val timeListener = TimePickerDialog.OnTimeSetListener {
_: TimePicker, hourOfDay: Int, minute: Int ->
val resultTime : Date =
GregorianCalendar(calendarYear, calendarMonth, calendarDay, hourOfDay, minute).time
targetFragment?.let { fragment ->
(fragment as Callbacks).onTimeSelected(resultTime)
}
}
val initialHour = calendar.get(HOUR_OF_DAY)
val initialMinute = calendar.get(MINUTE)
val is24HourView = false
return TimePickerDialog(
requireContext(),
timeListener,
initialHour,
initialMinute,
is24HourView
)
}
companion object {
fun newInstance(time: Date): TimePickerFragment {
val args = Bundle().apply {
putSerializable(ARG_TIME, time)
}
return TimePickerFragment().apply {
arguments = args
}
}
}
}
And here is CrimeFragment.kt:
package com.bignerdranch.android.criminalintent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import java.text.SimpleDateFormat
import java.util.*
private const val TAG = "CrimeFragment"
private const val ARG_CRIME_ID = "crime_id"
private const val DIALOG_DATE = "DialogDate"
private const val DIALOG_TIME = "DialogTime"
private const val REQUEST_DATE = 0
private const val REQUEST_TIME = 1
class CrimeFragment: Fragment(), DatePickerFragment.Callbacks, TimePickerFragment.Callbacks {
private lateinit var crime: Crime
private lateinit var titleField: EditText
private lateinit var dateButton: Button
private lateinit var timeButton: Button
private lateinit var solvedCheckBox: CheckBox
private val crimeDetailViewModel: CrimeDetailViewModel by lazy {
ViewModelProviders.of(this).get(CrimeDetailViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
crime = Crime()
val crimeId: UUID = arguments?.getSerializable(ARG_CRIME_ID) as UUID
crimeDetailViewModel.loadCrime(crimeId)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_crime, container, false)
titleField = view.findViewById(R.id.crime_title) as EditText
dateButton = view.findViewById(R.id.crime_date) as Button
timeButton = view.findViewById(R.id.crime_time) as Button
solvedCheckBox = view.findViewById(R.id.crime_solved) as CheckBox
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
crimeDetailViewModel.crimeLiveData.observe(
viewLifecycleOwner,
Observer { crime -> crime?.let { this.crime = crime
updateUI()
}
})
}
override fun onStart() {
super.onStart()
val titleWatcher = object : TextWatcher {
override fun beforeTextChanged(
sequence: CharSequence?,
start: Int,
count: Int,
after: Int
) {
// This space intentionally left blank
}
override fun onTextChanged(
sequence: CharSequence?,
start: Int,
before: Int,
count: Int
) {
crime.title = sequence.toString()
}
override fun afterTextChanged(sequence: Editable?) {
// This one too
}
}
titleField.addTextChangedListener(titleWatcher)
solvedCheckBox.apply {
setOnCheckedChangeListener { _, isChecked ->
crime.isSolved = isChecked
}
}
dateButton.setOnClickListener {
DatePickerFragment.newInstance(crime.date).apply {
setTargetFragment(this@CrimeFragment, REQUEST_DATE)
show(this@CrimeFragment.requireFragmentManager(), DIALOG_DATE)
}
}
timeButton.setOnClickListener {
TimePickerFragment.newInstance(crime.date).apply {
setTargetFragment(this@CrimeFragment, REQUEST_TIME)
show(this@CrimeFragment.requireFragmentManager(), DIALOG_TIME)
}
}
}
override fun onStop() {
super.onStop()
crimeDetailViewModel.saveCrime(crime)
}
override fun onDateSelected(date: Date) {
crime.date = date
updateUI()
}
override fun onTimeSelected(date: Date) {
crime.date = date
updateUI()
}
private fun updateUI() {
titleField.setText(crime.title)
val crimeDate = SimpleDateFormat("EEEE, MMM d, YYYY")
.format(this.crime.date)
dateButton.text = crimeDate
val crimeTime = SimpleDateFormat("hh:mm a")
.format(this.crime.date)
timeButton.text = crimeTime
solvedCheckBox.apply {
isChecked = crime.isSolved
jumpDrawablesToCurrentState()
}
}
companion object {
fun newInstance(crimeId: UUID): CrimeFragment {
val args = Bundle().apply {
putSerializable(ARG_CRIME_ID, crimeId)
}
return CrimeFragment().apply {
arguments = args
}
}
}
}