Calendar icon indicating copy to clipboard operation
Calendar copied to clipboard

Cancel setupAsync

Open akrulec opened this issue 2 years ago • 3 comments

I've tried using calendarView.setupAsync, but I've noticed that my view might be destroyed before this function completes. For example, I start drawing Fragment that contains the calendar, and I realize that the user should be redirected to the login view, so I navigate them there. I've noticed that I get a lot of null pointer exceptions, so I tried cancelling and cleaning the calendar view onDestroyView function, but that doesn't seem to cancel the job that starts the async thread. Is there a different call I can do on the destroy view to cancel the async job? Thanks

akrulec avatar Jun 22 '22 18:06 akrulec

The calendar cancels the job when the view is detached from the window, an alternative would be to return the job so the caller can cancel when appropriate. Can I see the stack trace of the NPEs you're getting? This way I can figure out what exactly is null, maybe I need to improve the internal job cleanup.

kizitonwose avatar Jun 28 '22 06:06 kizitonwose

I see the following error:

2022-06-28 14:52:15.680 23489-23559/com.mycopilot.copilotapp E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
    Process: com.mycopilot.copilotapp, PID: 23489
    java.lang.NullPointerException
        at com.mycopilot.copilotapp.today.calendar.CalendarFragment.getBinding(CalendarFragment.kt:61)
        at com.mycopilot.copilotapp.today.calendar.CalendarFragment.access$getBinding(CalendarFragment.kt:48)
        at com.mycopilot.copilotapp.today.calendar.CalendarFragment$onViewCreated$4.invoke(CalendarFragment.kt:99)
        at com.mycopilot.copilotapp.today.calendar.CalendarFragment$onViewCreated$4.invoke(CalendarFragment.kt:94)
        at com.kizitonwose.calendarview.CalendarView$setupAsync$1$1.invokeSuspend(CalendarView.kt:707)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loopOnce(Looper.java:226)
        at android.os.Looper.loop(Looper.java:313)
        at android.app.ActivityThread.main(ActivityThread.java:8663)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135)
    	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@e20508e, Dispatchers.Default]

My code looks like the following:

    // This property is only valid between onCreateView and onDestroyView.
    private val binding get() = _binding!!
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        _binding = FragmentCalendarBinding.bind(view)
        ...
        binding.calendarView.setupAsync(
            currentMonth.minusMonths(1),
            currentMonth.plusMonths(3),
            DayOfWeek.SUNDAY) {
            // Scroll to current date.
            binding.calendarView.scrollToDate(todayViewModel.uiState.value.currentDate)

It fails in this line where it's trying to scroll to view.

I also have the following code in the destroy:

   override fun onDestroyView() {
        super.onDestroyView()
        binding.calendarView.adapter = null
        _binding = null
    }

Thank you for looking into this.

akrulec avatar Jun 28 '22 21:06 akrulec

A workaround you can use for now is to call onDetachedFromWindow() on the calendarView before setting the binding to null. This allows the view to cancel the job internally. But because the method is currently protected you will not be able to call it directly. You need to extend the class to make the method public:

class MyCalendarView : CalendarView {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    public override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
    }
}

Then use the new class you created in your XML instead of CalendarView. Now you can call the method:

   override fun onDestroyView() {
        super.onDestroyView()
        binding.calendarView.onDetachedFromWindow()
        _binding = null
    }

kizitonwose avatar Jun 29 '22 18:06 kizitonwose

I've noticed that this error has come back although I'm using the suggested solution. This happens if I navigate away from the main fragment -- for example when I've detected that the user needs onboarding, or some other overlay.

binding.weekCalendarView.setupAsync(
            currentMonth.minusMonths(10),
            currentMonth.plusMonths(1),
            DayOfWeek.SUNDAY) {

            // Scroll to current date.
            selectedDate = todayViewModel.currentDate.value
            binding.weekCalendarView.scrollToDate(selectedDate)
            ...

On destroy function:

override fun onDestroyView() {
        super.onDestroyView()
        binding.weekCalendarView.onDetachedFromWindow()
        binding.weekCalendarView.adapter = null
        _binding = null
    }

Error

java.lang.NullPointerException: null cannot be cast to non-null type com.kizitonwose.calendarview.ui.CalendarAdapter
        at com.kizitonwose.calendarview.ui.CalendarLayoutManager.getAdapter(CalendarLayoutManager.kt:20)
        at com.kizitonwose.calendarview.ui.CalendarLayoutManager.access$getAdapter$p(CalendarLayoutManager.kt:16)
        at com.kizitonwose.calendarview.ui.CalendarLayoutManager$scrollToDay$1.run(CalendarLayoutManager.kt:52)

akrulec avatar Aug 10 '22 01:08 akrulec

This seems to be a bug in the library, the completion is still called even though the job is canceled. The fix will be to check if the job is active before calling the completion block in the library.

I will fix this in the library but not sure when, a workaround for you will be to check again in the completion block itself before calling scrollToDate. Something like this:

binding.weekCalendarView.setupAsync(
    currentMonth.minusMonths(10),
    currentMonth.plusMonths(1),
    DayOfWeek.SUNDAY) {

    // Scroll to current date.
    selectedDate = todayViewModel.currentDate.value
    // This should be a sufficient workaround
    if (_binding != null && binding.weekCalendarView.isAttachedToWindow)  { 
            binding.weekCalendarView.scrollToDate(selectedDate)
	    }
    }

kizitonwose avatar Aug 10 '22 07:08 kizitonwose

In version 1.1.0, a cancellation block is provided to cancel the job.

kizitonwose avatar Aug 29 '22 11:08 kizitonwose