The recommended way to do it in the documentation is much more verbose than what you seemed to be doing before creating your suspend function! This is because they recommend that you host your Dialog inside a DialogFragment (so it will be preserved under configuration changes or when the app is suspended and comes back).
If you don't mind the dialog disappearing when the screen rotates, you can skip that, but you are still left with having to use the listener.
I'm not exactly sure what you are describing when you say you have to pre-emptively declare variables. You still have to use the variable inside the listener, because the code outside the listener is called first. So really, your flow should look like this:
DatePickerDialog(requireActivity(), { _, pickedYear, pickedMonth, pickedDay ->
val schedDate = LocalDateTime.of(pickedYear, pickedMonth + 1, pickedDay)
TimePickerDialog(requireActivity(), { _, pickedHour, pickedMinute ->
val schedTime = LocalTime.of(pickedHour, pickedMinute)
// Use schedDate and schedTime in here....
// ...
// ...
}, initialHour, initialMinute, false).show()
}, initialYear, initialMonth, initialDay).show()
So yes, this nested callback looks horrible, and could be improved with suspend functions. But remember, this is not compatible with DialogFragments.
Here are some problems with your coroutine solution:
- DatePickerDialog's listener returns a 0-based month, but LocalDateTime uses a 1-based month, so you need to add 1 to the month.
- You used
suspendCoroutine
instead of suspendCancellableCoroutine
, so if your coroutine is cancelled, the stuff inside the lambda is leaked.
- Your suspend function doesn't handle the case where the user dismisses the dialog without choosing a date. If this happens, your coroutine will be stuck in a suspended state forever.
- You create the dialog on whatever background thread the coroutine might be using. I don't think this is supported. You need to be on the main thread to create and show the dialog.
- You launched your coroutine with
GlobalScope
instead of lifecycleScope
, so your coroutines leaks the Activity and the Fragment.
- You used
async
instead of launch
even though your coroutine isn't intended to return anything.
Here is how I would define the suspend function to handle cancellation and be called on the proper thread. I would also make it a Context extension function so you can reuse it in any activity or fragment.
suspend fun Context.getDateFromUser(initialDate: LocalDate = LocalDate.now()): LocalDate? = withContext(Dispatchers.Main) {
suspendCancellableCoroutine { cont ->
val onCancel = DialogInterface.OnCancelListener {
cont.resume(null)
}
val onSet = DatePickerDialog.OnDateSetListener { _, year, month, day ->
cont.resume(LocalDate.of(year, month + 1, day))
}
val dialog = DatePickerDialog(this@getDateFromUser, onSet, initialDate.year, initialDate.month -1, initialDate.dayOfMonth).apply {
onCancelListener = onCancel
show()
}
cont.invokeOnCancellation { dialog.cancel() }
}
}
In a fragment you might use it like this:
viewLifecycleOwner.lifecycleScope.launch {
val schedDate = requireActivity().getDateFromUser() ?: someDefaultLocalDate
val schedTime = requireActivity().getTimeFromUser() ?: someDefaultLocalTime
// Use schedDate and schedTime in here....
// ...
// ...
}