Skip to content

ADFA-3492 | Fix ProjectFile NPE in resource fragments#1341

Open
jatezzz wants to merge 1 commit into
stagefrom
fix/ADFA-3492-npe-resource-fragments
Open

ADFA-3492 | Fix ProjectFile NPE in resource fragments#1341
jatezzz wants to merge 1 commit into
stagefrom
fix/ADFA-3492-npe-resource-fragments

Conversation

@jatezzz
Copy link
Copy Markdown
Collaborator

@jatezzz jatezzz commented May 26, 2026

Description

Resolves a NullPointerException occurring in ColorFragment (and other resource fragments) when attempting to access ProjectFile methods like getColorsPath() on a null object reference. This PR introduces a ProjectResolver to safely resolve the project instance from ProjectManager or the fragment arguments. Additionally, it transitions the resource fragments to use the newInstance pattern, ensuring the ProjectFile is securely passed and retained across the fragment's lifecycle.

Details

  • Created ProjectResolver.java to handle safe project data extraction and display graceful error states if the project is null.
  • Updated ColorFragment, DrawableFragment, FontFragment, and StringFragment to use a newInstance(ProjectFile) method instead of relying solely on the singleton or non-default constructors.
  • Refactored ResourceManagerActivity to handle project intent loading correctly using IntentCompat and cleaned up its options menu action logic.

Before

Screen.Recording.2026-05-26.at.3.07.19.PM.mov

After

Screen.Recording.2026-05-26.at.3.40.42.PM.mov

Ticket

ADFA-3492

…agments

Introduces ProjectResolver to safely fetch project instances and handle null states
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Review Change Stack

📝 Walkthrough

Release Notes

Bug Fixes

  • Fixed NullPointerException in resource fragments (ColorFragment, DrawableFragment, FontFragment, StringFragment) that occurred when accessing ProjectFile methods on a null reference

Changes

Project Resolution Pattern

  • Introduced ProjectResolver utility class to safely obtain ProjectFile instances, eliminating direct ProjectManager singleton access from fragments
  • Fragments now use newInstance(ProjectFile) factory methods that pass project via Bundle arguments instead of relying on non-default constructors or singleton references
  • Project resolution with graceful error handling: displays error message and returns early if project is unavailable

Fragment Updates

  • ColorFragment: Added newInstance(ProjectFile) factory method; resolves project during onViewCreated()
  • DrawableFragment: Removed primary constructor parameter; added newInstance(ProjectFile) companion factory
  • FontFragment: Added newInstance(ProjectFile) factory method; project resolution moved to onViewCreated()
  • StringFragment: Added newInstance(ProjectFile) factory method; uses Constants.EXTRA_KEY_PROJECT for bundle key

ResourceManagerActivity Refactoring

  • Project initialization now uses IntentCompat.getParcelableExtra() with fallback to ProjectManager.instance.openedProject
  • Finishes activity if no project is available
  • Calls ProjectManager.instance.openProject() if project exists but not yet opened
  • Fragment creation updated to use newInstance(currentProject) pattern for all resource fragments
  • Menu action handling refactored with extracted helper methods: handleAddResourceAction(), handleViewXmlAction()
  • XML viewing for colors/strings now uses currentProject.colorsPath/currentProject.stringsPath instead of ProjectManager singletons

Risk Assessment & Best Practices

✓ Positive Patterns

  • Fragment arguments pattern for passing data across lifecycle events provides strong safety guarantees
  • Early returns in onViewCreated() when project is null prevents UI corruption
  • ProjectResolver abstraction encapsulates fallback logic and error handling consistently

⚠ Risks & Considerations

  • Singleton fallback dependency: ProjectResolver still relies on ProjectManager.instance.openedProject as fallback; singleton anti-pattern persists but is now contained in a utility class
  • Fragment lifecycle handling: Verify that fragments gracefully handle early exit when project is null (no partial UI initialization)
  • Intent extras handling: ResourceManagerActivity uses IntentCompat.getParcelableExtra() - ensure this doesn't break on older API levels if appropriate checks aren't in place
  • Error message display: Error states shown via SBUtils.make() - confirm these messages are user-friendly and don't create visual artifacts if shown repeatedly

Code Quality Metrics

  • Files modified: 6 (ResourceManagerActivity + 4 resource fragments + new ProjectResolver utility)
  • Lines changed: +252/-228 in ResourceManagerActivity; +31 lines for new ProjectResolver; +13/-2 per fragment update

Walkthrough

This PR refactors project resolution across resource management components. It introduces a ProjectResolver utility, updates four resource fragment classes to use a factory pattern with dependency injection via fragment arguments, and refactors ResourceManagerActivity to use these new factories and improve menu handling and file-picking logic.

Changes

Resource Fragment and Activity Refactoring

Layer / File(s) Summary
ProjectResolver utility foundation
layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/utils/ProjectResolver.java
ProjectResolver provides resolveProject(Bundle) to fetch project from ProjectManager.openedProject or extract from bundle extras, and getValidProjectOrShowError(Bundle, View) to resolve with optional error messaging.
Resource fragments factory pattern adoption
layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/fragments/resources/ColorFragment.java, DrawableFragment.kt, FontFragment.java, StringFragment.java
ColorFragment, DrawableFragment, FontFragment, and StringFragment each add newInstance(ProjectFile) static factory methods, update imports to add ProjectResolver and Constants while removing ProjectManager, and refactor onViewCreated to resolve project from fragment arguments via ProjectResolver with early return on resolution failure.
ResourceManagerActivity integration and refactoring
layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/activities/ResourceManagerActivity.kt
Activity initializes project from intent extra with fallback to ProjectManager.instance.openedProject, conditionally calls openProject() if needed, uses setupViewPager() to instantiate resource fragments via *.newInstance(currentProject) factories, refactors menu handling with extracted helpers (handleAddResourceAction, launchXmlViewer, showErrorState), and updates picker result handlers to locate active pager fragments by tag and validate selected content.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • appdevforall/CodeOnTheGo#727: Addresses DrawableFragment construction and instantiation patterns in ResourceManagerActivity.setupViewPager().
  • appdevforall/CodeOnTheGo#771: Refactors ResourceManagerActivity project-loading initialization and setupViewPager() fragment registration.

Suggested reviewers

  • itsaky-adfa
  • dara-abijo-adfa
  • Daniel-ADFA

Poem

🐰 Resources once scattered, now neat and refined,
Four fragments align in a factory design,
ProjectResolver hops in to guide the way,
Dependencies dance through arguments they convey,
ActivityManager orchestrates fragments with glee!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main issue (ProjectFile NPE) and the affected components (resource fragments), directly aligning with the core fix implemented across multiple fragment classes.
Description check ✅ Passed The description accurately documents the NPE issue, the ProjectResolver solution, the fragment refactoring strategy, and ResourceManagerActivity changes, all of which are reflected in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ADFA-3492-npe-resource-fragments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/activities/ResourceManagerActivity.kt`:
- Around line 63-65: The current logic uses
ProjectManager.instance.openedProject as primary and falls back to the intent,
which can ignore an explicitly requested project; change the assignment so
project is set from the IntentCompat.getParcelableExtra(...) result
(projectExtra) first, and then update/sync ProjectManager.instance.openedProject
to that project (or call ProjectManager.instance.openProject/openedProject
setter) so both the activity and ProjectManager reference the intent project;
apply the same change to the other occurrence(s) that currently use
openedProject ?: projectExtra (lines around the ProjectManager usage).
- Around line 205-207: The case for option 1 only calls launchPhotoPicker()
behind an API >= TIRAMISU check, so on API < 33 the option does nothing; update
the branch to handle legacy devices by invoking a fallback picker for older APIs
(e.g., call launchPhotoPicker() for API >= Build.VERSION_CODES.TIRAMISU and call
a new or existing legacy method such as launchPhotoPickerLegacy() or
startActivityForResult/Intent ACTION_PICK / ACTION_OPEN_DOCUMENT targeting
MediaStore.Images.Media.EXTERNAL_CONTENT_URI for API < TIRAMISU). Modify the
ResourceManagerActivity's selection handling (the switch/case that currently
references launchPhotoPicker()) and add the legacy picker helper (or reuse an
existing gallery/open-document helper) so image drawable selection works on
pre-Android 13 devices.

In
`@layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/fragments/resources/FontFragment.java`:
- Around line 71-73: onViewCreated may return early when
ProjectResolver.getValidProjectOrShowError(...) yields null leaving the field
executor uninitialized, so update onDestroyView to guard the teardown: check
that executor is non-null (and optionally not already shutdown/terminated)
before calling executor.shutdownNow(), and set executor to null after shutdown;
reference the executor field and the lifecycle methods
onViewCreated/onDestroyView and the ProjectResolver.getValidProjectOrShowError
call when making the change.

In
`@layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/utils/ProjectResolver.java`:
- Around line 15-18: resolveProject() currently prefers the singleton
ProjectManager.getInstance().getOpenedProject() which can ignore an explicitly
provided fragment argument; change the lookup order to first check
arguments.getParcelable(Constants.EXTRA_KEY_PROJECT) (or equivalent fragment
arguments retrieval) and return it if non-null, otherwise fall back to
ProjectManager.getInstance().getOpenedProject(); ensure null-safe handling of
arguments and the parcelable cast to avoid NPEs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f12afb07-7bb8-4223-b21c-0ce41d23d2f8

📥 Commits

Reviewing files that changed from the base of the PR and between 99d896f and 3ad5664.

📒 Files selected for processing (6)
  • layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/activities/ResourceManagerActivity.kt
  • layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/fragments/resources/ColorFragment.java
  • layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/fragments/resources/DrawableFragment.kt
  • layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/fragments/resources/FontFragment.java
  • layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/fragments/resources/StringFragment.java
  • layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/utils/ProjectResolver.java

Comment on lines +63 to +65
val projectExtra = IntentCompat.getParcelableExtra(intent, Constants.EXTRA_KEY_PROJECT, ProjectFile::class.java)
project = ProjectManager.instance.openedProject ?: projectExtra

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the intent project as the primary source and sync ProjectManager accordingly.

The current precedence (openedProject ?: projectExtra) can ignore the explicitly requested project and show/edit the wrong project.

💡 Proposed fix
 val projectExtra = IntentCompat.getParcelableExtra(intent, Constants.EXTRA_KEY_PROJECT, ProjectFile::class.java)
-project = ProjectManager.instance.openedProject ?: projectExtra
+project = projectExtra ?: ProjectManager.instance.openedProject

 if (project == null) {
     finish()
     return@launch
 }

-if (ProjectManager.instance.openedProject == null) {
-    ProjectManager.instance.openProject(project)
+project?.let { resolvedProject ->
+    if (ProjectManager.instance.openedProject?.path != resolvedProject.path) {
+        ProjectManager.instance.openProject(resolvedProject)
+    }
 }

Also applies to: 71-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/activities/ResourceManagerActivity.kt`
around lines 63 - 65, The current logic uses
ProjectManager.instance.openedProject as primary and falls back to the intent,
which can ignore an explicitly requested project; change the assignment so
project is set from the IntentCompat.getParcelableExtra(...) result
(projectExtra) first, and then update/sync ProjectManager.instance.openedProject
to that project (or call ProjectManager.instance.openProject/openedProject
setter) so both the activity and ProjectManager reference the intent project;
apply the same change to the other occurrence(s) that currently use
openedProject ?: projectExtra (lines around the ProjectManager usage).

Comment on lines +205 to +207
1 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
launchPhotoPicker()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Image Drawable selection is a no-op on pre-Android 13 devices.

On API < 33, option 1 does nothing, so users cannot add image drawables from this picker.

💡 Proposed fix
-1 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-    launchPhotoPicker()
-}
+1 -> {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+        launchPhotoPicker()
+    } else {
+        photoPicker?.launch("image/*")
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/activities/ResourceManagerActivity.kt`
around lines 205 - 207, The case for option 1 only calls launchPhotoPicker()
behind an API >= TIRAMISU check, so on API < 33 the option does nothing; update
the branch to handle legacy devices by invoking a fallback picker for older APIs
(e.g., call launchPhotoPicker() for API >= Build.VERSION_CODES.TIRAMISU and call
a new or existing legacy method such as launchPhotoPickerLegacy() or
startActivityForResult/Intent ACTION_PICK / ACTION_OPEN_DOCUMENT targeting
MediaStore.Images.Media.EXTERNAL_CONTENT_URI for API < TIRAMISU). Modify the
ResourceManagerActivity's selection handling (the switch/case that currently
references launchPhotoPicker()) and add the legacy picker helper (or reuse an
existing gallery/open-document helper) so image drawable selection works on
pre-Android 13 devices.

Comment on lines +71 to +73
project = ProjectResolver.getValidProjectOrShowError(getArguments(), view);
if (project == null) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Guard executor teardown when project resolution fails.

If project is null, onViewCreated returns before executor is initialized; onDestroyView() then calls shutdownNow() on null and can crash.

💡 Proposed fix
 `@Override`
 public void onDestroyView() {
   super.onDestroyView();
-  executor.shutdownNow();
+  if (executor != null) {
+    executor.shutdownNow();
+    executor = null;
+  }
   binding = null;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/fragments/resources/FontFragment.java`
around lines 71 - 73, onViewCreated may return early when
ProjectResolver.getValidProjectOrShowError(...) yields null leaving the field
executor uninitialized, so update onDestroyView to guard the teardown: check
that executor is non-null (and optionally not already shutdown/terminated)
before calling executor.shutdownNow(), and set executor to null after shutdown;
reference the executor field and the lifecycle methods
onViewCreated/onDestroyView and the ProjectResolver.getValidProjectOrShowError
call when making the change.

Comment on lines +15 to +18
ProjectFile openedProject = ProjectManager.getInstance().getOpenedProject();

if (openedProject != null) return openedProject;
if (arguments != null) return arguments.getParcelable(Constants.EXTRA_KEY_PROJECT);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prefer fragment arguments over singleton state in project resolution.

resolveProject() currently returns openedProject first, which can ignore the explicitly injected fragment argument and resolve the wrong project.

💡 Proposed fix
 public static ProjectFile resolveProject(`@Nullable` Bundle arguments) {
-    ProjectFile openedProject = ProjectManager.getInstance().getOpenedProject();
-
-    if (openedProject != null) return openedProject;
-    if (arguments != null) return arguments.getParcelable(Constants.EXTRA_KEY_PROJECT);
-
-    return null;
+    if (arguments != null) {
+        ProjectFile fromArgs = arguments.getParcelable(Constants.EXTRA_KEY_PROJECT);
+        if (fromArgs != null) return fromArgs;
+    }
+    return ProjectManager.getInstance().getOpenedProject();
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@layouteditor/src/main/java/org/appdevforall/codeonthego/layouteditor/utils/ProjectResolver.java`
around lines 15 - 18, resolveProject() currently prefers the singleton
ProjectManager.getInstance().getOpenedProject() which can ignore an explicitly
provided fragment argument; change the lookup order to first check
arguments.getParcelable(Constants.EXTRA_KEY_PROJECT) (or equivalent fragment
arguments retrieval) and return it if non-null, otherwise fall back to
ProjectManager.getInstance().getOpenedProject(); ensure null-safe handling of
arguments and the parcelable cast to avoid NPEs.

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.

1 participant