kizitonwose / Calendar

A highly customizable calendar view and compose library for Android.
MIT License
4.5k stars 492 forks source link

NullPointerException: null cannot be cast to non-null type com.kizitonwose.calendar.view.internal.weekcalendar.WeekCalendarAdapter #463

Closed akrulec closed 9 months ago

akrulec commented 1 year ago

Library information:

Describe the bug

Error when opening the application which inflates this class:

java.lang.NullPointerException: null cannot be cast to non-null type com.kizitonwose.calendar.view.internal.weekcalendar.WeekCalendarAdapter at com.kizitonwose.calendar.view.internal.weekcalendar.WeekCalendarLayoutManager.getAdapter(WeekCalendarLayoutManager.kt:13)

To Reproduce (if applicable)

Steps to reproduce the behavior: Open the App

kizitonwose commented 1 year ago

Steps to reproduce the behavior: Open the App

Which app is this? The sample app?

akrulec commented 1 year ago

Oh, sorry for a poor explanation. I was migrating from 1.0 to 2.0 version of the calendar in the app. Every once in a while when I open the app (my app), the app crashes and I see this error.

My XML:

<androidx.fragment.app.FragmentContainerView
            android:id="@+id/week_calendar_fragment"
            android:name="com.xxx.ui.today.calendar.WeekCalendarFragment"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/banner"/>
<com.kizitonwose.calendar.view.WeekCalendarView
            android:id="@+id/week_calendar_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/calendar_toolbar"
            app:cv_dayViewResource="@layout/item_view_calendar_day"
            app:cv_outDateStyle="endOfRow"
            app:cv_orientation="horizontal"
            app:cv_scrollPaged="true"
            android:transitionGroup="true"
            android:transitionName="@{@string/calendar_week_transition_name}"/>

Code in the Fragment:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        _binding = FragmentCalendarWeekBinding.bind(view)

        binding.weekCalendarView.apply {
            // In case the user enters from the landscape, just take the smallest number.
            dayWidth = min(dm.widthPixels, dm.heightPixels) / NUM_DAYS_TO_SHOW
            daySize = DaySize.SeventhWidth
            dayBinder = CVDayBinder()
        }

        val localDate = LocalDate.now()
        binding.weekCalendarView.setup(
            localDate.minusMonths(10), localDate.plusMonths(2), DayOfWeek.SUNDAY)

            binding.weekCalendarView.doOnAttach { binding.weekCalendarView.scrollToDate(selectedDate) }

        // Each time user scrolls back and forth, refresh the data.
        binding.weekCalendarView.weekScrollListener =
            object : WeekScrollListener {
                override fun invoke(week: Week) {
                    todayViewModel.getHabitDays(week.days.first().date, week.days.last().date)

                    // Because Sunday is the first day, we need to % number of days.
                    // Otherwise it doesn't work correctly if the user selected a Sunday.
                    val dayOfWeek = (selectedDate.dayOfWeek.value % NUM_DAYS_TO_SHOW)
                    // Scroll to the same day in the previous/next week.
                    todayViewModel.setDate(week.days[dayOfWeek].date)
                }
            }

...

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

    /**
     * Class representing Day view binder. It is responsible for binding [HabitDay] to the given day
     * in the calendar.
     */
    inner class CVDayBinder : WeekDayBinder<DayViewContainer> {
        override fun bind(container: DayViewContainer, data: WeekDay) =
            container.bind(data.date, selectedDate, null)

        override fun create(view: View) =
            DayViewContainer(
                view, requireContext(), viewLifecycleOwner, todayViewModel, dayWidth) { date ->
                    selectNewDay(date)
                }
    }

The crash error only points to the errors in the kizitonwose part of the code. So not sure what part of my code would cause this. Thank you

kizitonwose commented 1 year ago

This is because you remove the adapter which is managed internally and something from your code outlives the view lifecycle (could be scroll listener etc) and tries to access the calendar which causes the crash. You need to remove this binding.weekCalendarView.adapter = null from your onDestroyView(). If you must keep it, you need to set the internal layout manager to null as well.

akrulec commented 1 year ago

Understood, so if I want to keep it, scroll listener should be notified as well? I guess, chasing the leak canary leaks and removing/nullifying everything when the view is destroy is the reason why the adapter was set to null explicitly. Is there a better way of 'cleaning up' and making sure things don't outlive the view? Thank you

kizitonwose commented 1 year ago

Maybe something like this could work:

binding.weekCalendarView.adapter = null
binding.weekCalendarView.layoutManager = null
binding.weekCalendarView.weekScrollListener = null
kizitonwose commented 1 year ago

Hey @akrulec is this resolved?

akrulec commented 1 year ago

I have tried a version where I only set binding to null in on destroy:

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

and I still see crashes, so I'm going to try a version now that clears all three objects before setting binding to null.

kizitonwose commented 9 months ago

@akrulec Any updates on this?

akrulec commented 9 months ago

Yes, sorry for late response. I removed everything from onDestroy, and there is no more leaks and no more crashes. Just nullifying the _binding works.

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