In my android application, I am using Kotlin Android Extensions
for accessing views from XML layout inside fragments and activities. This works well in almost all scenarios but I am constantly getting a null pointer crash report on crashlytics when the app is in use of actual users. I am unable to reproduce this issue or understand the cause of this issue.
The crash is on a fragment that has Kotlin Android Extensions
usage.
Below is my code:
Kotlin File
import kotlinx.android.synthetic.main.fragment_documents.*
@Keep
class BottomNavFragment : Fragment() {
private val activityViewModel: MainActivityViewModel by activityViewModels()
lateinit var mainActivity: MainActivity
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val rootView = inflater.inflate(R.layout.fragment_documents, container, false)
mainActivity = activity as MainActivity
return rootView
}
companion object {
@JvmStatic
fun newInstance() =
BottomNavFragment().apply {
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
try {
setUpTabLayout()
handleViewModelObservers()
} catch (ex: Exception) {
ex.printStackTrace()
}
}
private fun handleViewModelObservers() {
activityViewModel.isBackToHomeEnabled.observe(viewLifecycleOwner,
{ isHomeEnabled ->
if (isHomeEnabled!!) {
tabLayout.getTabAt(1)?.select()
}
})
}
private fun setUpTabLayout() {
try {
if (mainActivity != null && activityViewModel != null && viewPager != null &&tabLayout!=null) {
val adapter = FilesViewPagerAdapter(childFragmentManager)
adapter.addFragments()
viewPager.adapter = adapter
viewPager.offscreenPageLimit = 4
viewPager.currentItem = 1
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
mainActivity.clearSearch()
}
override fun onPageScrollStateChanged(state: Int) {
}
})
tabLayout.setupWithViewPager(viewPager)
tabLayout.getTabAt(0)?.icon = mainActivity.getDrawable(R.drawable.test1)
tabLayout.getTabAt(1)?.icon = mainActivity.getDrawable(R.drawable.test2)
tabLayout.getTabAt(2)?.icon = mainActivity.getDrawable(R.drawable.test3)
tabLayout.getTabAt(3)?.icon = mainActivity.getDrawable(R.drawable.test3)
tabLayout.getTabAt(4)?.icon = mainActivity.getDrawable(R.drawable.test5)
tabLayout.getTabAt(0)?.text = getString(R.string.test1)
tabLayout.getTabAt(1)?.text = getString(R.string.test2)
tabLayout.getTabAt(2)?.text = getString(R.string.test3)
tabLayout.getTabAt(3)?.text = getString(R.string.test4)
tabLayout.getTabAt(4)?.text = getString(R.string.test5)
}
} catch (ex: Exception) {
ex.printStackTrace()
}
}
}
XML Layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".views.fragments.BottomNavFragment">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="@dimen/_50sdp"
android:background="@color/colorPrimary" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/_60sdp"
android:layout_marginStart="@dimen/_10sdp"
android:layout_marginTop="@dimen/_20sdp"
android:layout_marginEnd="@dimen/_10sdp"
android:background="@drawable/bg_round_view_white"
app:tabSelectedTextColor="@color/colorPrimary"
app:tabTextAppearance="@style/ThemeTextAppearanceTab">
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/tabLayout" />
</RelativeLayout>
Crash Report
Fatal Exception: java.lang.NullPointerException: viewPager must not be null
at mypackagename.views.fragments.BottomNavFragment$setUpTabLayout$1.run(BottomNavFragment.java)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:145)
at android.app.ActivityThread.main(ActivityThread.java:6946)
at java.lang.reflect.Method.invoke(Method.java)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1404)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1199)
What I have learned so far:
Kotlin Android Extensions actually have findViewById
, findViewCache
and clearFindViewByIdCache
implementation.
And as mentioned in this article
The problem with fragments is that the view can be recreated but the fragment instance will be kept alive. What happens then? This means that the views inside the cache would be no longer valid.
So, how I can know that the view is no longer valid? Or, what I can do to prevent this crash?
Any help will be appreciated.
Android extensions plugin is deprecated and the official proposal is to use ViewBinding
instead which provided compile time access to the views of an xml layout without the need to do viewFindById
on each view you have.
This page here will provide you with the steps to set up your code to use view binding, make sure to check the code snippet on a fragment to clear the reference you have on the views