Skip to content

Fixing the initialisation issues of categoryImagesCallback due to CategoryDetailsActivity, WikidataItemDetailsActivity not using reformed fragments#6747

Open
mnlch wants to merge 5 commits intocommons-app:mainfrom
mnlch:main
Open

Fixing the initialisation issues of categoryImagesCallback due to CategoryDetailsActivity, WikidataItemDetailsActivity not using reformed fragments#6747
mnlch wants to merge 5 commits intocommons-app:mainfrom
mnlch:main

Conversation

@mnlch
Copy link

@mnlch mnlch commented Mar 17, 2026

Description

Fixes #6358

"What changes did you make and why?":
Inside the setTabs function (called through onCreate) of both *DetailsActivity classes, I switched from initialising the fragments directly

depictedImagesListFragment = DepictedImagesFragment()
// ...

to only initialising if the fragment is currently not in the supportFragmentManager, meaning that it was not yet created/loaded in the current lifecycle:

depictedImagesListFragment = fragments
    .filterIsInstance<DepictedImagesFragment>()
    .firstOrNull()
    ?: DepictedImagesFragment()

This is because during the reformation of the fragments and activities after a theme switch, the fragments apparently get loaded first, meaning that in the default behaviour, the loaded (and initialised) instances would be overwritten by one which does not load before being queried for the categoryImagesCallback. When this occurs, however, the fragments are already present as children inside the supportFragmentManager, meaning we can pull them out. I'll explain further in the discussion of issue #6358 to leave a breadcrumb trail for anyone encountering this issue again.

I deemed the filterIsInstance method as the most elegant method of pulling these children out of the manager. Other methods would be to index fragments[i] (children seem to always be in the same order they were added) or use the find by tag function, looking for "android:switcher:{viewPage id}:{i}", in accordance to this undocumented behaviour.

Furthermore, I also renamed the variable depictionImagesListFragment to depictedImagesListFragment, in accordance to its class, DepictedImagesFragment.

PS: that weird little merge in the commit history is because a typo I wanted to amend, but my IDE turned into into a merge hiccup :(

Tests performed

As per the issue description, I tested this non-automatically on the Pixel 9 Pro emulator, running API 36.

I have tested the following scenarios:

  • Click on an image that you have submitted, click on a category, then switch dark mode in the menu and click any of the items in the list. No longer crashes.
  • Click on an image from the 'Explore' tab, click on a category, then switch dark mode in the menu and click any of the items in the list. No longer crashes.
  • On an image from the 'Explore' tab, click on an item listed next to 'Depictions', then switch dark mode in the menu and click any of the items in the list. No longer crashes.

mnlch added 4 commits March 17, 2026 00:42
…preventing uninitialised access to categoryImagesCallback (6358)
…enting uninitialised access to categoryImagesCallback (6358)
…enting uninitialised access to categoryImagesCallback (6358)
# Conflicts:
#	app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt
val parentCategoriesFragment = fragments
.filterIsInstance<ParentCategoriesFragment>()
.firstOrNull()
?: ParentCategoriesFragment()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this approach please? It seems the PR tries to do something else and not related to the issue likely LLM can you verify this solves the issue?.

@neeldoshii
Copy link
Collaborator

@mnlch Can you check why the test cases are failing, I am making this PR as draft once its ready for review feel free to make it ready for review.

@neeldoshii neeldoshii marked this pull request as draft March 18, 2026 17:22
@mnlch
Copy link
Author

mnlch commented Mar 18, 2026

I: as for the CI failure: I had renamed variable depictionImagesListFragment to depictedImagesListFragment because it bugged me that it didnt have the same name as its type DepictedImagesFragment. New commit should rectify this -- unit tests pass locally for beta {debug, production}.

II: I'll explain you more how I found this error, so you can understand why I chose this fix:

As described in my comment, which I mention in the PR, I found it by tracing. Here is my last trace log, which I created by adding Timber.d at points in the stacktrace (not committed -- spams the log output):

2026-03-18 21:16:49.146  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  tr16-03 PageableMediaFragment.kt enters onAttach 180744664
2026-03-18 21:16:49.146  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  > tr16-03 at this point, categoryImagesCallback IS initialised
2026-03-18 21:16:49.149  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  tr16-03 Entered pagedListAdapter, and categoryImagesCallback initialisation = true in 180744664
2026-03-18 21:16:49.155  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  tr16-03 PageableMediaFragment.kt enters onAttach 94321720
2026-03-18 21:16:49.156  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  > tr16-03 at this point, categoryImagesCallback IS initialised
2026-03-18 21:16:49.157  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  tr16-03 Entered pagedListAdapter, and categoryImagesCallback initialisation = true in 94321720
2026-03-18 21:16:50.863  6586-6586  MediaDetai...erFragment fr.free.nrw.commons.beta             D  tr16-03 inside onCreate initProvider, is about to be called 124030868
2026-03-18 21:16:50.864  6586-6586  MediaDetai...erFragment fr.free.nrw.commons.beta             D  tr16-03 initProvider chose parentFragment 124030868
2026-03-18 21:16:50.864  6586-6586  MediaDetai...erFragment fr.free.nrw.commons.beta             D  tr16-03 MediaDetailPagerFragment.onCreateView starts 124030868
2026-03-18 21:16:50.866  6586-6586  MediaDetai...erFragment fr.free.nrw.commons.beta             D  tr16-03 setting adapter to 97672067,fr.free.nrw.commons.media.MediaDetailAdapter@5d25b83 inside 124030868
2026-03-18 21:16:53.793  6586-6586  WikidataIt...lsActivity fr.free.nrw.commons.beta             D  tr16-03 enter WikidataItemDetailsActivity.onCreate 170381035
2026-03-18 21:16:53.817  6586-6586  WikidataIt...lsActivity fr.free.nrw.commons.beta             D  tr16-03 leave WikidataItemDetailsActivity.setTabs 170381035
2026-03-18 21:16:53.817  6586-6586  WikidataIt...lsActivity fr.free.nrw.commons.beta             D  tr16-03 leave WikidataItemDetailsActivity.onCreate 170381035
2026-03-18 21:16:53.825  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  tr16-03 PageableMediaFragment.kt enters onAttach 223918495
2026-03-18 21:16:53.825  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  tr16-03 PageableMediaFragment.kt uses activity as callback 223918495 -> 170381035
2026-03-18 21:16:53.826  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  > tr16-03 at this point, categoryImagesCallback IS initialised
2026-03-18 21:16:53.826  6586-6586  DepictedImagesFragment  fr.free.nrw.commons.beta             D  tr16-03 enter DepictedImagesFragment.onAttach 223918495
2026-03-18 21:16:53.829  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  tr16-03 Entered pagedListAdapter, and categoryImagesCallback initialisation = true in 223918495
2026-03-18 21:16:53.830  6586-6586  DepictedImagesFragment  fr.free.nrw.commons.beta             D  tr16-03 enter DepictedImagesFragment.onViewCreated 223918495
<<< HERE I SWITCH THEMES TO CAUSE THE BUG >>>
2026-03-18 21:17:09.142  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  tr16-03 PageableMediaFragment.kt enters onAttach 106097950
2026-03-18 21:17:09.142  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  tr16-03 PageableMediaFragment.kt uses activity as callback 106097950 -> 267238214
2026-03-18 21:17:09.142  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  > tr16-03 at this point, categoryImagesCallback IS initialised
2026-03-18 21:17:09.143  6586-6586  DepictedImagesFragment  fr.free.nrw.commons.beta             D  tr16-03 enter DepictedImagesFragment.onAttach 106097950
2026-03-18 21:17:09.147  6586-6586  WikidataIt...lsActivity fr.free.nrw.commons.beta             D  tr16-03 enter WikidataItemDetailsActivity.onCreate 267238214
2026-03-18 21:17:09.147  6586-6586  WikidataIt...lsActivity fr.free.nrw.commons.beta             D  tr16-03 WikidataItemDetailsActivity.onCreate contained a saved state 267238214
2026-03-18 21:17:09.155  6586-6586  WikidataIt...lsActivity fr.free.nrw.commons.beta             D  tr16-03 leave WikidataItemDetailsActivity.setTabs 267238214
2026-03-18 21:17:09.155  6586-6586  WikidataIt...lsActivity fr.free.nrw.commons.beta             D  tr16-03 leave WikidataItemDetailsActivity.onCreate 267238214
2026-03-18 21:17:09.158  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  tr16-03 Entered pagedListAdapter, and categoryImagesCallback initialisation = true in 106097950
2026-03-18 21:17:09.158  6586-6586  DepictedImagesFragment  fr.free.nrw.commons.beta             D  tr16-03 enter DepictedImagesFragment.onViewCreated 106097950
2026-03-18 21:17:10.701  6586-6586  WikidataIt...lsActivity fr.free.nrw.commons.beta             D  tr16-03 entered WikidataItemDetailsActivity.onMediaClicked, in the mediaDetailPagerFragment fixer, current obj is null inside 267238214
2026-03-18 21:17:10.701  6586-6586  WikidataIt...lsActivity fr.free.nrw.commons.beta             D  > tr16-03 has created 173671352 inside 267238214
2026-03-18 21:17:10.703  6586-6586  MediaDetai...erFragment fr.free.nrw.commons.beta             D  tr16-03 inside onCreate initProvider, is about to be called 173671352
2026-03-18 21:17:10.703  6586-6586  MediaDetai...erFragment fr.free.nrw.commons.beta             D  tr16-03 initProvider chose activity fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity@fedbb46,267238214 inside 173671352
2026-03-18 21:17:10.704  6586-6586  MediaDetai...erFragment fr.free.nrw.commons.beta             D  tr16-03 MediaDetailPagerFragment.onCreateView starts 173671352
2026-03-18 21:17:10.705  6586-6586  MediaDetai...erFragment fr.free.nrw.commons.beta             D  tr16-03 setting adapter to 247209363,fr.free.nrw.commons.media.MediaDetailAdapter@ebc1d93 inside 173671352
2026-03-18 21:17:10.705  6586-6586  PageableMediaFragment   fr.free.nrw.commons.beta             D  tr16-03 Entered pagedListAdapter, and categoryImagesCallback initialisation = false in 164467152

Take note:

  1. enter, leave mean what you think they do.
  2. The number at the end of each line is the hashcode() of each object. I added this after noticing that certain statements were executed multiple times, so I can check if it was the same instance doing this.
  3. "... uses X as callback ..." and "... chose ..." is in reference to this code, because I suspected this was causing issues:
// PageabkeMediaFragment.kt
override fun onAttach(context: Context) {
    super.onAttach(context)
    Timber.d("tr16-03 PageableMediaFragment.kt enters onAttach %d", hashCode())
    if (parentFragment != null) {
        categoryImagesCallback = (parentFragment as CategoryImagesCallback)
    } else {
        Timber.d("tr16-03 PageableMediaFragment.kt uses activity as callback %d -> %d", hashCode(), activity.hashCode())
        categoryImagesCallback = (activity as CategoryImagesCallback)
    }
    if (this::categoryImagesCallback.isInitialized) {
        Timber.d("> tr16-03 at this point, categoryImagesCallback IS initialised")
    }
}

// MediaDetailPagerFragment.kt
private fun initProvider() {
    if (parentFragment is MediaDetailProvider) {
        Timber.d("tr16-03 initProvider chose parentFragment %d", hashCode())
        mediaDetailProvider = parentFragment as MediaDetailProvider
    } else if (activity is MediaDetailProvider) {
        Timber.d("tr16-03 initProvider chose activity %s,%d inside %d", activity, activity.hashCode(), hashCode())
        mediaDetailProvider = activity as MediaDetailProvider?
    } else {
        throw ClassCastException("Parent must implement MediaDetailProvider")
    }
}
  1. WikidataItemDetailsActivity is of type MediaDetailProvider and CategoryImagesCallback. It builds a view pager using the fragment DepictedImagesFragment, which is of type PageableMediaFragment, which requires a reference to an object of type CategoryImagesCallback. Hence, WikidataItemDetailsActivity and DepictedImagesFragment both have a pointer to each other. CategoryImagesCallback makes a new object MediaDetailPagerFragment, which after onCreate adds a pointer to its parent or its containing activity. The next step, onCreateView, sets the adapter and cascades into the lateinit property ... has not been initialized error, as MediaDetailPagerFragment calls MediaDetailAdapter calls back MediaDetailPagerFragment dereferences WikidataItemDetailsActivity calls DepictedImagesFragment dereferences WikidataItemDetailsActivity on its categoryImagesCallback field, which is not initialised. You can see why I called this a 'wild goosechase', hopefully.

What can you see in the logs?

  • The PageableMediaFragment (obj 106097950) DOES initialise properly, and initialises categoryImagesCallback.
  • When MediaDetailPagerFragment starts the cascade that leads to the issue, it creates a new object, 164467152, which is uninitialised.

What do I conclude from these logs?

  • There is a lifecycle dependency: WikidataItemDetailsActivity.onCreate (calling upon categoryImagesCallback) must happen after PageableMediaFragment.onAttach (initialising categoryImagesCallback).
  • The first time the application is created, the activity and fragment are created in the assumed order.
  • When switching modes, the fragment is reloaded (onAttach) before the activity (onCreateView) (see trace). This causes the activity to overwrite the existing (properly initialised) fragment, and instead uses an unitialised object, which you can see in the old code:
// WikidataItemDetailsActivity.kt
private fun setTabs() {
    depictionImagesListFragment = DepictedImagesFragment()
    val childDepictionsFragment = ChildDepictionsFragment()
    val parentDepictionsFragment = ParentDepictionsFragment()
    // ...
}

So what was my solution?

  • If Android loads the fragments out-of-order, read them out of the supportFragmentManager, which keeps track of them. As I already mentioned, you can also grab them directly as supportFragmentManager.fragments[i], but this furthermore relies on the implicit assumption that Android will place the fragments back in the same order as they were created in.

Ultimately, I think the real issue is the logical complexity of this code. A lot of objects reference each other through their supertypes, which makes finding relatively easy issues (sequential coupling) quite obtuse.

@neeldoshii
Copy link
Collaborator

@RitikaPahwa4444 Can you run the copilot on this PR.

@neeldoshii neeldoshii marked this pull request as ready for review March 19, 2026 07:14
@RitikaPahwa4444 RitikaPahwa4444 requested a review from Copilot March 19, 2026 07:15
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses crash #6358 by ensuring CategoryDetailsActivity and WikidataItemDetailsActivity reuse already-restored ViewPager fragments after configuration/theme changes, preventing fragment fields from pointing at “fresh” unattached instances that lack initialized callbacks.

Changes:

  • Rework setTabs() in CategoryDetailsActivity and WikidataItemDetailsActivity to prefer existing fragments from FragmentManager over always instantiating new ones.
  • Rename depictionImagesListFragment to depictedImagesListFragment for consistency with DepictedImagesFragment.
  • Update the corresponding unit test to reflect the renamed field.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.kt Reuse restored fragments in setTabs() and rename the DepictedImages fragment field.
app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt Reuse restored fragments in setTabs() to avoid overwriting restored instances.
app/src/test/kotlin/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivityUnitTests.kt Update test to use the renamed depictedImagesListFragment field.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +101 to +105
val fragments = supportFragmentManager!!.fragments
depictedImagesListFragment = fragments
.filterIsInstance<DepictedImagesFragment>()
.firstOrNull()
?: DepictedImagesFragment()
Comment on lines +122 to 124
depictedImagesListFragment!!.arguments = arguments
parentDepictionsFragment.arguments = arguments
childDepictionsFragment.arguments = arguments
Comment on lines +91 to +95
val fragments = supportFragmentManager.fragments
categoriesMediaFragment = fragments
.filterIsInstance<CategoriesMediaFragment>()
.firstOrNull()
?: CategoriesMediaFragment()
Comment on lines +91 to +95
val fragments = supportFragmentManager.fragments
categoriesMediaFragment = fragments
.filterIsInstance<CategoriesMediaFragment>()
.firstOrNull()
?: CategoriesMediaFragment()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UninitializedPropertyAccessException for lateinit property categoryImagesCallback

3 participants