0

We have a random crash on production in this class when accessing the binding at line 10 :

class BulletinFragment : Fragment(R.layout.fragment_bulletins) { 
    private val bulletinViewModel: BulletinsViewModel by viewModel() 
    private val binding by viewBinding(FragmentBulletinsBinding::bind) 

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 
        super.onViewCreated(view, savedInstanceState) 
        viewLifecycleOwner.lifecycleScope.launch {
             viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 
                 bulletinViewModel.switchState.collect { 
                      binding.bulletinLiveNotificationsBanner.switch.isSelected = it 
                 } 
             } 
        } 
    } 
} 

viewModel is provided by Koin, and the binding delegate is Zhuiden's one from here

class FragmentViewBindingDelegate<T : ViewBinding>(
    val fragment: Fragment,
    val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
    private var _binding: T? = null

    init {
        fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onCreate(owner: LifecycleOwner) {
                fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
                    viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
                        override fun onDestroy(owner: LifecycleOwner) {
                            _binding = null
                        }
                    })
                }
            }
        })
    }

    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
        val binding = _binding
        if (binding != null) {
            return binding
        }
        val lifecycle = fragment.viewLifecycleOwner.lifecycle
        if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
            throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
        }
        return viewBindingFactory(thisRef.requireView()).also { _binding = it }
    }
}

fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
    FragmentViewBindingDelegate(this, viewBindingFactory)

inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
    crossinline bindingInflater: (LayoutInflater) -> T
): Lazy<T> {
    return lazy(LazyThreadSafetyMode.NONE) {
        bindingInflater.invoke(layoutInflater)
    }
}

This fragment is called within a viewPager2:

class CartPagerAdapter(fragment: Fragment) : FragmentStateAdapter(
    fragment.childFragmentManager,
    fragment.viewLifecycleOwner.lifecycle
) {

    val firstFragment = FirstFragment()
    val secondFragment = SecondFragment()

    override fun createFragment(position: Int): Fragment = when (position) {
        Tab.FIRST_TAB.tabIndex -> firstFragment
        Tab.SECOND_TAB.tabIndex -> secondFragment
        Tab.THIRD_TAB.tabIndex -> BulletinFragment()
        else -> error("The fragment position should in 0 < x < 2 but was '$position'")
    }

    fun handleDeeplink(deeplink: Uri) {
        when (deeplink.host) {
            FIRST_TAB_DEEPLINK_HOST -> firstFragment.handleDeeplink(deeplink)
            SECOND_TAB_DEEPLINK_HOST -> secondFragment.handleDeeplink(deeplink)
        }
    }

    override fun getItemCount(): Int = 3
}
class CartHomeFragment : Fragment(R.layout.fragment_cart_home), CartHomeContract.View {

    private var tabLayoutMediator: TabLayoutMediator? = null

    private val args: CartHomeFragmentArgs by navArgs()

    private var initTab: Int? = null

    // betSlip needs to scroll to top when displaying QrCodes tab (set when moving to QrCode tab after validating cart)
    private val pagerAdapter: CartPagerAdapter by adapter { CartPagerAdapter(this) }

    private val binding by viewBinding(FragmentCartHomeBinding::bind)
    private val scope
        get() = viewLifecycleOwner.lifecycleScope

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initTab = args.tabIndex
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initialize()
    }

    override fun onDestroyView() {
        tabLayoutMediator?.detach()
        tabLayoutMediator = null
        super.onDestroyView()
    }

    private fun initialize() {
        scope.launch {
            binding.fragmentCartHomeViewPager.run {
                adapter = pagerAdapter
                setPagerCurrentItem(this)
                offscreenPageLimit = 2
            }

            tabLayoutMediator = TabLayoutMediator(binding.cartTabLayout, binding.fragmentCartHomeViewPager) { tab, position ->
                val (title, contentDesc) = with(getTab(position)) { getString(title) to getString(contentDesc) }
                tab.text = title
                tab.contentDescription = contentDesc
            }
            tabLayoutMediator?.attach()
        }
    }

    private fun setPagerCurrentItem(viewPager: ViewPager2) {
        val initialIntent = arguments?.getParcelable<Intent>(NavController.KEY_DEEP_LINK_INTENT)
        initialIntent?.data?.let {
            pagerAdapter.handleDeeplink(it)
            viewPager.setCurrentItemForDeeplink(it.host)
            initialIntent.data = null
        } ?: run {
            initTab?.let {
                viewPager.setCurrentItem(it, false)
                initTab = null
            }
        }
    }

    fun getTab(index: Int): Tab {
        return Tab.values()[index]
    }


    private fun ViewPager2.setCurrentItemForDeeplink(deeplink: String?) {
        setCurrentItem(Tab.getTabIndexForDeeplink(deeplink), false)
    }

    companion object {
        const val FIRST_TAB_DEEPLINK_HOST = "first"
        const val SECOND_TAB_DEEPLINK_HOST = "second"
        const val THIRD_TAB_DEEPLINK_HOST = "third"
        val DEFAULT_TAB: Tab = Tab.FIRST_TAB

        const val SECOND_TAB_DEEPLINK_DETAILS_PATH = "/details"
    }

}

enum class Tab(@StringRes val title: Int, @StringRes val contentDesc: Int, val deeplink: String) {
    FIRST_TAB(R.string.first_tab_tab_title, R.string.a11y_first_tab, FIRST_TAB_DEEPLINK_HOST),
    SECOND_TAB(R.string.second_tab_title, R.string.a11y_second_tab, SECOND_TAB_DEEPLINK_HOST),
    THIRD_TAB(R.string.third_tab_title, R.string.a11y_third_tab, THIRD_TAB_DEEPLINK_HOST);

    val tabIndex: Int = ordinal

    companion object {
        fun getTabIndexForDeeplink(deeplink: String?): Int =
            (values().firstOrNull { it.deeplink == deeplink }
                ?: DEFAULT_TAB)
                .tabIndex
    }
}

In the BulletinFragment, I know that the repeatOnLifecycle block seems useless here but we need it for some reason that is not necessary to explain here. I just would like to understand what is wrong with this piece of code. Actually, we get from crashlytics the following crash happening randomly (rare enough to not succeed to reproduce it, but frequent enough to significantly decrease the crashfree):

Fatal Exception: java.lang.IllegalStateException Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView() 
androidx.fragment.app.Fragment.getViewLifecycleOwner (Fragment.java:377) 
com.mycompany.myapp.common.tools.FragmentViewBindingDelegate.getValue (FragmentViewBindingDelegate.kt:40) 
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment.<clinit> (BulletinFragment.kt:18) 
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment.access$getBinding (BulletinFragment.java:15) 
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment$onViewCreated$2$1$1.emit (BulletinFragment.kt:25) 
com.mycompany.myapp.feature.cart.home.bulletin.BulletinFragment$onViewCreated$2$1$1.emit (BulletinFragment.kt:24) 
com.mycompany.myapp.domain.usecase.notifications.LiveNotificationsUseCase$getSwitchStateFlow$$inlined$map$1$2.emit (Emitters.kt:227) 
com.mycompany.myapp.domain.usecase.notifications.LiveNotificationsUseCase$getSwitchStateFlow$$inlined$map$1$2$1.invokeSuspend (Emitters.kt:12) 
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33) 
kotlinx.coroutines.internal.DispatchedContinuation.resumeWith (DispatchedContinuation.kt:205) 
kotlin.coroutines.SafeContinuation.resumeWith (SafeContinuationJvm.kt:41) 

How can we endup with this crash when

  • we tie the coroutine with the viewLifecycleOwner lifecycleScope
  • and the collect is done inside a block where the lifecycleOwner state is STARTED, the lifecycleOwner being the view if I properly understand.

How the view can be null in this case ??? Is it related to the ViewPager2

Uguzon
  • 31
  • 4
  • 1
    Is there a reason your `CartPagerAdapter` isn't just passing the Fragment directly to the `FragmentStateAdapter` that takes a `Fragment`? You are not using the right Lifecycle there (which is exactly why there is a `Fragment` constructor - to do the right thing). – ianhanniballake Nov 23 '22 at 16:13

0 Answers0