Fix: safely access binding in MediaDetailFragment to prevent crash on delayed callbacks

This commit is contained in:
Dev Jadiya 2025-06-18 09:37:17 +05:30
parent 154cb0fa67
commit 0c6a0c5307

View file

@ -190,8 +190,13 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
private var initialListTop: Int = 0
/**
* Holds the view binding reference for this fragment.
* Use bindingOrNull for safe access after view destruction.
*/
private var _binding: FragmentMediaDetailBinding? = null
private val binding get() = _binding!!
private val bindingOrNull get() = _binding
private var descriptionHtmlCode: String? = null
@ -340,25 +345,27 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
handleBackEvent(view)
//set onCLick listeners
binding.mediaDetailLicense.setOnClickListener { onMediaDetailLicenceClicked() }
binding.mediaDetailCoordinates.setOnClickListener { onMediaDetailCoordinatesClicked() }
binding.sendThanks.setOnClickListener { sendThanksToAuthor() }
binding.dummyCaptionDescriptionContainer.setOnClickListener { showCaptionAndDescription() }
binding.mediaDetailImageView.setOnClickListener {
launchZoomActivity(
binding.mediaDetailImageView
)
/**
* Safely sets click listeners on media detail UI elements using bindingOrNull.
* Prevents potential crashes caused by view being destroyed during delayed callbacks.
*/
bindingOrNull?.let { binding ->
binding.mediaDetailLicense.setOnClickListener { onMediaDetailLicenceClicked() }
binding.mediaDetailCoordinates.setOnClickListener { onMediaDetailCoordinatesClicked() }
binding.sendThanks.setOnClickListener { sendThanksToAuthor() }
binding.dummyCaptionDescriptionContainer.setOnClickListener { showCaptionAndDescription() }
binding.mediaDetailImageView.setOnClickListener {
launchZoomActivity(binding.mediaDetailImageView)
}
binding.categoryEditButton.setOnClickListener { onCategoryEditButtonClicked() }
binding.depictionsEditButton.setOnClickListener { onDepictionsEditButtonClicked() }
binding.seeMore.setOnClickListener { onSeeMoreClicked() }
binding.mediaDetailAuthor.setOnClickListener { onAuthorViewClicked() }
binding.nominateDeletion.setOnClickListener { onDeleteButtonClicked() }
binding.descriptionEdit.setOnClickListener { onDescriptionEditClicked() }
binding.coordinateEdit.setOnClickListener { onUpdateCoordinatesClicked() }
binding.copyWikicode.setOnClickListener { onCopyWikicodeClicked() }
}
binding.categoryEditButton.setOnClickListener { onCategoryEditButtonClicked() }
binding.depictionsEditButton.setOnClickListener { onDepictionsEditButtonClicked() }
binding.seeMore.setOnClickListener { onSeeMoreClicked() }
binding.mediaDetailAuthor.setOnClickListener { onAuthorViewClicked() }
binding.nominateDeletion.setOnClickListener { onDeleteButtonClicked() }
binding.descriptionEdit.setOnClickListener { onDescriptionEditClicked() }
binding.coordinateEdit.setOnClickListener { onUpdateCoordinatesClicked() }
binding.copyWikicode.setOnClickListener { onCopyWikicodeClicked() }
binding.fileUsagesComposeView.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
@ -404,14 +411,19 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
/**
* Gets the height of the frame layout as soon as the view is ready and updates aspect ratio
* of the picture.
* Updated to use bindingOrNull inside delayed callbacks to prevent crashes
* when view is destroyed before post/postDelayed is executed.
*/
view.post{
val width = binding.mediaDetailScrollView.width
view.post {
val safeBinding = bindingOrNull ?: return@post
val width = safeBinding.mediaDetailScrollView.width
if (width > 0) {
frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight
frameLayoutHeight = safeBinding.mediaDetailFrameLayout.measuredHeight
updateAspectRatio(width)
} else {
view.postDelayed({ updateAspectRatio(binding.root.width) }, 1)
view.postDelayed({
bindingOrNull?.let { updateAspectRatio(it.root.width) }
}, 1)
}
}
@ -526,19 +538,25 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
binding.sendThanks.visibility = View.VISIBLE
}
binding.mediaDetailScrollView.viewTreeObserver.addOnGlobalLayoutListener(
/**
* Sets a one-time global layout listener to safely access view dimensions.
* Uses bindingOrNull to avoid crash if the fragment view is destroyed
* before the callback is executed.
*/
bindingOrNull?.mediaDetailScrollView?.viewTreeObserver?.addOnGlobalLayoutListener(
object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
val safeBinding = bindingOrNull ?: return
if (context == null) {
return
}
binding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener(
safeBinding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener(
this
)
oldWidthOfImageView = binding.mediaDetailScrollView.width
if (media != null) {
oldWidthOfImageView = safeBinding.mediaDetailScrollView.width
media?.filename?.let {
displayMediaDetails()
fetchFileUsages(media?.filename!!)
fetchFileUsages(it)
}
}
}
@ -548,23 +566,28 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
binding.mediaDetailScrollView.viewTreeObserver.addOnGlobalLayoutListener(
/**
* Sets a global layout listener to update the aspect ratio when device configuration changes.
* Uses bindingOrNull to avoid crashes in case the view is destroyed before callback.
*/
bindingOrNull?.mediaDetailScrollView?.viewTreeObserver?.addOnGlobalLayoutListener(
object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
val safeBinding = bindingOrNull ?: return
/**
* We update the height of the frame layout as the configuration changes.
*/
binding.mediaDetailFrameLayout.post {
frameLayoutHeight = binding.mediaDetailFrameLayout.measuredHeight
updateAspectRatio(binding.mediaDetailScrollView.width)
safeBinding.mediaDetailFrameLayout.post {
frameLayoutHeight = safeBinding.mediaDetailFrameLayout.measuredHeight
updateAspectRatio(safeBinding.mediaDetailScrollView.width)
}
if (binding.mediaDetailScrollView.width != oldWidthOfImageView) {
if (safeBinding.mediaDetailScrollView.width != oldWidthOfImageView) {
if (newWidthOfImageView == 0) {
newWidthOfImageView = binding.mediaDetailScrollView.width
newWidthOfImageView = safeBinding.mediaDetailScrollView.width
updateAspectRatio(newWidthOfImageView)
}
binding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener(
this
safeBinding.mediaDetailScrollView.viewTreeObserver.removeOnGlobalLayoutListener(
this
)
}
}
@ -784,9 +807,13 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
}
}
/**
* Clears binding and removes layout listener to prevent memory leaks
* or crashes from delayed UI callbacks after the view is destroyed.
*/
override fun onDestroyView() {
if (layoutListener != null && view != null) {
requireView().viewTreeObserver.removeGlobalOnLayoutListener(layoutListener) // old Android was on crack. CRACK IS WHACK
layoutListener?.let {
view?.viewTreeObserver?.removeOnGlobalLayoutListener(it)
layoutListener = null
}
@ -2263,4 +2290,4 @@ fun FileUsagesContainer(
}
}
}
}