Skip to content

Conversation

@shubham1g5
Copy link
Contributor

@shubham1g5 shubham1g5 commented Apr 21, 2025

Product Description

https://dimagi.atlassian.net/browse/CCCT-955

We were traditinally using Fragments with setRetainInstance(true) to store state and handle Android configuration changes. Although setRetainInstance has been deprecated and the new recommended approach is to use View Models for surving state on congiguration changes. This PR removes our ccurrent non UI fragments that we were using to handle state and replace them with view models.

On the connect side of things, we were noticing that it's already not working as expected with dialog fragments not getting re-attached to re-created activity context after configuration change. We could not pinpoint it to any change made by us in connect and therefore we decided to shift to a more reliable way to handle rotation.

This PR should not cause any UI changes.

Safety Assurance

Safety story

There are various parts that this PR affects that are risk to break because of these changes -

  1. Alert Dialogs: There are a lot of these throughout the app, some examples are about commcare dialog, language selection dialog, form exit dialog, repeat group dialog, form error dialog, wifi direct dialog. I think we have good testing in bunch of these areas if not all. In addition, I have confirmed that these set of dialogs doesn't get killed on config changes like rotation. A lot of these dialogs are covered as part of our instrumentation tests as well.
  2. Breadcrumb bar/ Persistent Case Tiles: Should be covered by TilesTests
  3. Progress Dialogs like on clicking sync, app update: Tested these and verified they retain instane on rotation. Also verified that progress transition fron one async task to other async task (like form upload to restore) works as expected.
  4. Case list screen: Verified that the entity list doesn't reload on rotation.

Labels and Review

  • Do we need to enhance the manual QA test coverage ? If yes, the "QA Note" label is set correctly
  • Does the PR introduce any major changes worth communicating ? If yes, the "Release Note" label is set and a "Release Note" is specified in PR description.
  • Risk label is set correctly
  • The set of people pinged as reviewers is appropriate for the level of risk of the change

QA Note: Do rotation testing on all CommCare screens, progress dialogs and other popups and make sure we don't reload the UI causing a small lag after rotation and that the dialog buttons work correctly after rotation

@coderabbitai
Copy link

coderabbitai bot commented Apr 21, 2025

📝 Walkthrough

Walkthrough

This set of changes refactors state retention and dialog management throughout the application. Fragment-based state holders such as ContainerFragment and TaskConnectorFragment are replaced with their ViewModel-based counterparts (ContainerViewModel and TaskConnectorViewModel), impacting activities including CommCareActivity, CommCareSetupActivity, and EntitySelectActivity. The breadcrumb bar UI component is similarly refactored, with BreadcrumbBarFragment replaced by BreadcrumbBarViewModel. All logic for saving and restoring UI state, background tasks, and breadcrumb bar management is updated to use the ViewModel architecture, removing fragment transactions and lifecycle dependencies.

Additionally, dialog construction and display methods are standardized. Dialog classes such as CommCareAlertDialog, StandardAlertDialog, and PaneledChoiceDialog are updated to require an explicit context when showing dialogs. All invocations of showNonPersistentDialog() across the codebase are updated to pass the relevant activity or context. Some dialog builder logic is moved from subclasses to the superclass. These changes streamline dialog lifecycle management and improve consistency across the application.

Sequence Diagram(s)

sequenceDiagram
    participant Activity
    participant ViewModelProvider
    participant ContainerViewModel
    participant TaskConnectorViewModel
    participant BreadcrumbBarViewModel

    Note over Activity: On Activity creation
    Activity->>ViewModelProvider: get(ContainerViewModel)
    ViewModelProvider-->>Activity: ContainerViewModel instance

    Activity->>ViewModelProvider: get(TaskConnectorViewModel)
    ViewModelProvider-->>Activity: TaskConnectorViewModel instance

    Activity->>ViewModelProvider: get(BreadcrumbBarViewModel)
    ViewModelProvider-->>Activity: BreadcrumbBarViewModel instance

    Note over Activity,ContainerViewModel: Activity saves/restores state via ViewModel
    Note over Activity,TaskConnectorViewModel: Activity manages background tasks via ViewModel
    Note over Activity,BreadcrumbBarViewModel: Activity manages breadcrumb bar via ViewModel
Loading
sequenceDiagram
    participant Activity
    participant DialogClass

    Note over Activity: When showing a dialog
    Activity->>DialogClass: create dialog instance
    Activity->>DialogClass: showNonPersistentDialog(Activity)
    DialogClass->>Activity: buildDialog(Activity)
    DialogClass->>Activity: show()
Loading

Suggested labels

skip-integration-tests

Suggested reviewers

  • avazirna

Tip

⚡💬 Agentic Chat (Pro Plan, General Availability)
  • We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 6

🔭 Outside diff range comments (1)
app/src/org/commcare/views/dialogs/AlertDialogFragment.java (1)

49-58: ⚠️ Potential issue

Missing null‑guards can crash on cancel / dismiss

underlyingDialog is dereferenced in onCancel() and onDismiss() without a
null‑check.
If neither the fragment argument nor the ViewModel contained a dialog (see
edge‑case above), a rotation followed by a user‑initiated cancel will raise an
NPE.

@Override
public void onCancel(DialogInterface dialog) {
    super.onCancel(dialog);
-    underlyingDialog.performCancel(dialog);
+    if (underlyingDialog != null) {
+        underlyingDialog.performCancel(dialog);
+    }
}

Mirror the same guard in onDismiss.

♻️ Duplicate comments (1)
app/src/org/commcare/activities/EntitySelectActivity.java (1)

872-873: Same leak‑risk when saving adapter

containerViewModel.setData(adapter); repeats the leak noted above. Prefer
serialising lightweight state instead.

🧹 Nitpick comments (7)
app/src/org/commcare/views/dialogs/AlertDialogFragment.java (1)

60-70: Raw ViewModel type loses generic information

get(ContainerViewModel.class) returns ContainerViewModel as a raw type,
forcing an unchecked cast later. Suppress the warning or keep the type:

ContainerViewModel<CommCareAlertDialog> viewModel =
        new ViewModelProvider(requireActivity())
                .get(ContainerViewModel.class);   // unchecked

(Optional) Create a typed ViewModelProvider.Factory so you can call
get(ContainerViewModel.class) safely without suppressions.

app/src/org/commcare/activities/EntitySelectActivity.java (1)

341-348: Guard against duplicate observer registration

When the adapter is restored from the ViewModel, setupUIFromAdapter() is
called, but later in onResumeSessionSafe() the same adapter registers the
DataSetObserver again. If the activity repeatedly rotates, the observer
will be registered multiple times, leading to redundant callbacks.

Add a boolean flag or unregister first:

if (adapter != null) {
-    adapter.registerDataSetObserver(mListStateObserver);
+    adapter.unregisterDataSetObserver(mListStateObserver);
+    adapter.registerDataSetObserver(mListStateObserver);
}
app/src/org/commcare/fragments/TaskConnectorViewModel.java (2)

16-19: Spelling: use “detach” instead of “deattach”
Minor but worth fixing to keep Javadoc professional and searchable.

- * An activity is responsible to attach and deattach itself
+ * An activity is responsible to attach and detach itself

88-91: Raw return type breaks generics safety
Return CommCareTask<?, ?, ?, R> instead of raw CommCareTask.

-    public CommCareTask getCurrentTask() {
+    public CommCareTask<?, ?, ?, R> getCurrentTask() {
app/src/org/commcare/views/dialogs/CommCareAlertDialog.java (1)

80-85: Potential double setView call
buildDialog() sets the view and then finalizeView() sets it again.
This is harmless but redundant and slightly wasteful.

app/src/org/commcare/fragments/BreadcrumbBarViewModel.java (2)

60-75: Duplicate method name causes confusion
attachBreadcrumbBar(CommCareActivity) calls a private helper with the same name (attachBreadcrumbBar(AppCompatActivity, ActionBar)), making stack traces and code search harder. Consider renaming the helper to configureBreadcrumbActionBar(...).


286-293: Null‑handling fallback may hide upstream errors
When buildContextTile returns null, the code silently falls back to requestEntityContext(). If the primary call failed due to an unexpected condition (e.g. bad session state), the problem is swallowed. Add logging to track the first failure for easier debugging.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c25d05 and a23cdd7.

📒 Files selected for processing (26)
  • app/src/org/commcare/activities/CallOutActivity.java (2 hunks)
  • app/src/org/commcare/activities/CommCareActivity.java (14 hunks)
  • app/src/org/commcare/activities/CommCareSetupActivity.java (6 hunks)
  • app/src/org/commcare/activities/DrawActivity.java (1 hunks)
  • app/src/org/commcare/activities/EntitySelectActivity.java (5 hunks)
  • app/src/org/commcare/activities/FormEntryActivityUIController.java (1 hunks)
  • app/src/org/commcare/activities/GeoPointActivity.java (1 hunks)
  • app/src/org/commcare/activities/HomeScreenBaseActivity.java (1 hunks)
  • app/src/org/commcare/activities/LoginActivity.java (1 hunks)
  • app/src/org/commcare/fragments/BreadcrumbBarViewModel.java (9 hunks)
  • app/src/org/commcare/fragments/ContainerFragment.java (0 hunks)
  • app/src/org/commcare/fragments/ContainerViewModel.java (1 hunks)
  • app/src/org/commcare/fragments/TaskConnectorViewModel.java (2 hunks)
  • app/src/org/commcare/preferences/AdvancedActionsPreferences.java (1 hunks)
  • app/src/org/commcare/preferences/MainConfigurablePreferences.java (1 hunks)
  • app/src/org/commcare/utils/AndroidShortcuts.java (2 hunks)
  • app/src/org/commcare/utils/GeoUtils.java (1 hunks)
  • app/src/org/commcare/utils/Permissions.java (2 hunks)
  • app/src/org/commcare/utils/TemplatePrinterUtils.java (2 hunks)
  • app/src/org/commcare/views/dialogs/AlertDialogFragment.java (3 hunks)
  • app/src/org/commcare/views/dialogs/CommCareAlertDialog.java (3 hunks)
  • app/src/org/commcare/views/dialogs/CustomViewAlertDialog.java (1 hunks)
  • app/src/org/commcare/views/dialogs/PaneledChoiceDialog.java (0 hunks)
  • app/src/org/commcare/views/dialogs/StandardAlertDialog.java (0 hunks)
  • app/src/org/commcare/views/widgets/ImageWidget.java (1 hunks)
  • app/unit-tests/src/org/commcare/utils/RobolectricUtil.java (2 hunks)
💤 Files with no reviewable changes (3)
  • app/src/org/commcare/views/dialogs/StandardAlertDialog.java
  • app/src/org/commcare/views/dialogs/PaneledChoiceDialog.java
  • app/src/org/commcare/fragments/ContainerFragment.java
🧰 Additional context used
🧬 Code Graph Analysis (6)
app/src/org/commcare/activities/LoginActivity.java (1)
app/src/org/commcare/views/dialogs/DialogCreationHelpers.java (1)
  • DialogCreationHelpers (22-77)
app/src/org/commcare/activities/EntitySelectActivity.java (1)
app/src/org/commcare/fragments/ContainerViewModel.java (1)
  • ContainerViewModel (5-16)
app/src/org/commcare/views/dialogs/AlertDialogFragment.java (1)
app/src/org/commcare/fragments/ContainerViewModel.java (1)
  • ContainerViewModel (5-16)
app/src/org/commcare/fragments/TaskConnectorViewModel.java (2)
app/src/org/commcare/activities/CommCareActivity.java (1)
  • CommCareActivity (83-979)
app/src/org/commcare/tasks/templates/CommCareTask.java (1)
  • CommCareTask (13-223)
app/src/org/commcare/activities/CommCareSetupActivity.java (1)
app/src/org/commcare/fragments/ContainerViewModel.java (1)
  • ContainerViewModel (5-16)
app/src/org/commcare/fragments/BreadcrumbBarViewModel.java (2)
app/src/org/commcare/views/EntityViewTile.java (1)
  • EntityViewTile (46-491)
app/src/org/commcare/utils/AndroidUtil.java (1)
  • AndroidUtil (19-65)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Lint Code Base
🔇 Additional comments (41)
app/src/org/commcare/fragments/ContainerViewModel.java (1)

1-16: Well-designed ViewModel implementation for data retention

This new generic ContainerViewModel class is a clean implementation that follows the Android ViewModel architecture pattern. It's a simple yet effective container for storing and retrieving data that needs to survive configuration changes.

app/src/org/commcare/preferences/AdvancedActionsPreferences.java (1)

293-293: Updated dialog invocation with explicit context parameter

The change to pass the activity context explicitly to showNonPersistentDialog() aligns with best practices for dialog management and is part of the wider refactoring to improve lifecycle handling.

app/src/org/commcare/activities/FormEntryActivityUIController.java (1)

590-590: Updated dialog invocation with explicit context parameter

The change to pass the activity context explicitly to showNonPersistentDialog() improves dialog lifecycle management and prevents potential memory leaks.

app/src/org/commcare/preferences/MainConfigurablePreferences.java (1)

140-140: Updated dialog invocation with explicit context parameter

The change to pass the activity context explicitly via getActivity() to showNonPersistentDialog() follows the same pattern as other dialog invocations throughout the codebase, ensuring consistent dialog lifecycle management.

app/src/org/commcare/utils/AndroidShortcuts.java (1)

58-58: Appropriate context passing in dialog display.

The update to pass the current activity instance (this) to showNonPersistentDialog() aligns with the PR's goal of removing reliance on No UI Fragments with setRetainInstance. This explicit context passing ensures proper dialog lifecycle management.

app/src/org/commcare/activities/DrawActivity.java (1)

323-323: Consistent dialog context management.

Passing the activity context explicitly to showNonPersistentDialog() ensures proper dialog lifecycle management and is consistent with changes throughout the codebase. This change supports the removal of Fragment-based state retention.

app/src/org/commcare/utils/TemplatePrinterUtils.java (2)

154-154: Explicit context passing for dialog display.

The addition of the activity parameter to showNonPersistentDialog() ensures proper dialog management without relying on retained instance state in fragments.


168-168: Consistent context management pattern.

This change follows the same pattern as other dialog calls in the codebase, ensuring proper lifecycle management by explicitly passing the activity context to the dialog.

app/src/org/commcare/utils/Permissions.java (1)

53-53: Improved dialog lifecycle management.

Explicitly passing the activity context to showNonPersistentDialog() aligns with the architectural change to remove reliance on retained fragment instances for state preservation. This ensures dialogs are properly associated with their parent activity's lifecycle.

app/src/org/commcare/utils/GeoUtils.java (2)

119-119: Consistent API update for dialog management.

The change updates the dialog method to explicitly receive the activity context, which aligns with the broader refactoring effort to improve dialog lifecycle management across the codebase.


119-119: Properly associates dialog with activity lifecycle

The change explicitly passes the activity context to the showNonPersistentDialog method, which ensures that the dialog is correctly associated with the activity's lifecycle and prevents potential memory leaks.

app/src/org/commcare/activities/GeoPointActivity.java (2)

238-238: Properly passing activity context to dialog method.

This change ensures the dialog is properly associated with its parent activity lifecycle, which helps prevent potential memory leaks and improves state handling during configuration changes.


238-238: Improved dialog lifecycle management

Explicitly passing the activity context to showNonPersistentDialog ensures the dialog is properly tied to the activity lifecycle, which follows the broader refactor to remove retained fragments in favor of ViewModel-based state management.

app/src/org/commcare/activities/CallOutActivity.java (4)

117-117: Dialog method updated to use explicit activity context.

This change follows the pattern of passing the activity instance to dialog methods, maintaining consistency with the codebase-wide refactoring from fragment-based state retention to ViewModel architecture.


135-135: Updated dialog method to receive activity context.

Similar to other dialog invocations, this change properly supplies the activity context to the dialog method, ensuring proper lifecycle attachment.


117-117: Proper dialog context association

Explicitly passing the activity to showNonPersistentDialog ensures the dialog is properly tied to the activity lifecycle, preventing potential memory leaks and improper view attachment.


135-135: Proper dialog context association

Similarly to the change above, passing the activity context to the permission request dialog ensures proper lifecycle management, which aligns with the effort to eliminate retained fragments for state storage.

app/src/org/commcare/activities/LoginActivity.java (2)

433-433: Updated dialog display method to explicitly pass activity context.

The change properly supplies the activity instance to the showNonPersistentDialog method, which aligns with the broader refactoring to improve dialog lifecycle management throughout the application.


433-433: Improved dialog lifecycle handling

Passing the activity context to showNonPersistentDialog is consistent with the codebase-wide pattern of explicitly providing the context for better lifecycle management. This ensures the About dialog is properly associated with the activity and can be managed correctly during configuration changes.

app/src/org/commcare/views/widgets/ImageWidget.java (1)

110-110: Dialog call updated to pass context parameter

The call to showNonPersistentDialog() has been updated to explicitly pass the context parameter, aligning with the broader refactoring throughout the codebase that improves dialog lifecycle management by using explicit context references.

app/src/org/commcare/activities/HomeScreenBaseActivity.java (1)

504-504: Dialog call updated to pass activity context

The dialog invocation has been modified to explicitly pass the activity instance (this) to the showNonPersistentDialog method, which is consistent with the broader pattern of improving dialog lifecycle management throughout the codebase.

app/src/org/commcare/views/dialogs/CustomViewAlertDialog.java (2)

16-16: Simplified dialog view configuration

The constructor implementation has been refactored to delegate view configuration to the superclass method setView(view) instead of managing an internal AlertDialog.Builder instance, which simplifies the code and improves maintainability.


20-21: Improved button configuration pattern

The setPositiveButton implementation now delegates to the inherited methods setPositiveButtonText() and setPositiveButtonListener() instead of directly managing an internal builder. This change is part of the overall dialog refactoring to improve lifecycle management and code organization.

app/unit-tests/src/org/commcare/utils/RobolectricUtil.java (3)

3-7: Updated imports for ViewModel-based architecture

Import statements have been updated to support the new ViewModel-based architecture, replacing the fragment-based imports with the appropriate androidx lifecycle components.


24-26: Migrated from fragment to ViewModel for state management

The implementation has been updated to use ViewModelProvider to obtain TaskConnectorViewModel instead of accessing state through fragments. This aligns with the architectural shift from fragment-based state holders to ViewModel-based lifecycle-aware components throughout the codebase.


33-33: Fixed main looper handling

The ShadowLooper.idleMainLooper() call has been moved outside the conditional block to ensure it's always called after attempting to get the task result, regardless of whether a task was found.

app/src/org/commcare/activities/CommCareSetupActivity.java (2)

239-242: Good migration to an Activity‑scoped ViewModel

persistCommCareAppState() correctly restores the CommCareApp instance from
ContainerViewModel, ensuring it survives rotations without the old retained
fragment.


464-465: Potential leak: heavy object in ViewModel

CommCareApp is added to the ContainerViewModel. If CommCareApp holds a
reference to the Activity (directly or indirectly), retaining it may leak the
old activity after a rotation. Verify CommCareApp is free of such references
(or store a lightweight ID instead).

app/src/org/commcare/activities/CommCareActivity.java (13)

30-35: Architecture modernization: Updated imports for ViewModel-based approach.

The import statements have been updated to include the ViewModel-related libraries from AndroidX, replacing fragment-based imports with ViewModel-based counterparts. This shift aligns with Android's recommended architecture components.


39-41: Switched to ViewModel-based state management.

The imports have been updated to use ViewModels instead of Fragment-based state holders:

  • BreadcrumbBarViewModel instead of BreadcrumbBarFragment
  • ContainerViewModel instead of ContainerFragment
  • TaskConnectorViewModel instead of TaskConnectorFragment

This is a significant architectural improvement that better aligns with Android's recommended patterns for state management across lifecycle events.


93-93: Updated field declaration to use ViewModel.

Changed the state holder from a Fragment-based implementation to a ViewModel-based implementation, which is better for handling lifecycle events and configuration changes.


131-131: Renamed fields to use ViewModels.

Fragment-based fields have been replaced with their ViewModel counterparts:

  • containerFragmentcontainerViewModel
  • breadcrumbBarFragmentbreadcrumbBarViewModel

This ensures UI state is preserved across configuration changes like screen rotations.

Also applies to: 135-135


146-148: Implemented ViewModel instantiation and attachment.

The stateHolder is now retrieved using ViewModelProvider and explicitly attached to the activity. This follows the proper lifecycle-aware pattern for ViewModels, ensuring the activity can properly connect to background tasks.


172-174: Refactored breadcrumb bar initialization to use ViewModel.

Instead of creating and managing a fragment transaction, the code now uses a ViewModel to handle the breadcrumb bar state. The attachBreadcrumbBar method centralizes the attachment logic in the ViewModel itself.


184-190: Improved UI state persistence with ViewModels.

The persistManagedUiState method now uses ViewModelProvider to create or obtain the ContainerViewModel, ensuring UI state is properly preserved across configuration changes. This provides a more reliable mechanism than fragment-based state retention.


326-326: Enhanced state saving in onPause.

UI state is now saved to the ContainerViewModel's data store during onPause, ensuring that state is preserved in a lifecycle-aware manner, which is more reliable than the previous fragment-based approach.


340-340: Added proper cleanup in onDestroy.

The stateHolder is now explicitly detached in onDestroy, preventing potential memory leaks by properly breaking the reference between the activity and the ViewModel when the activity is destroyed.


824-827: Updated back press handling to use ViewModel.

The onBackPressed method now checks with the breadcrumbBarViewModel instead of a fragment to determine if a tile is expanded. This centralizes the breadcrumb state management in the ViewModel.


904-904: Refactored action bar refresh to use ViewModel.

The refreshActionBar method now calls attachBreadcrumbBar on the breadcrumbBarViewModel instead of refreshing a fragment, centralizing the UI update logic in the ViewModel.


949-949: Modified breadcrumb bar display behavior.

Changed shouldShowBreadcrumbBar to always return false by default, effectively disabling the breadcrumb bar display unless specifically overridden by subclasses. This suggests the breadcrumb bar functionality is now opt-in rather than default.


936-938: Added setter for dependency injection.

Added a setStateHolder method to allow for dependency injection of the TaskConnectorViewModel, improving testability and flexibility of the activity by allowing the state holder to be mocked or injected.

Comment on lines 50 to 55
private TabbedDetailView mInternalDetailView = null;
private View tile;

private final static String INLINE_TILE_COLLAPSED = "collapsed";
private final static String INLINE_TILE_EXPANDED = "expanded";

Copy link

@coderabbitai coderabbitai bot Apr 21, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

ViewModel stores View instances – risk of Activity leaks
mInternalDetailView and tile hold strong references to UI elements created with an Activity context. When the activity is destroyed these references keep it (and its entire window hierarchy) alive, defeating the purpose of migrating away from retained fragments.

Recommendation:

  1. Move all view creation/expansion logic back to the activity (or a dedicated Fragment).
  2. Keep the ViewModel pure: only retain the data needed to rebuild the breadcrumb bar, not the views themselves.
  3. Provide observable state (e.g. LiveData) for the activity to render.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

would these been causing leaks even in previous code with fragment ? Trying to verify if it was an existing issue vs a regression with view model changes ?

I am wary of changing this code too much as it's a bit legacy and I don't have a good understanding of it and would like to make minimal changes here if possible while avoiding a memory leak.

Copy link

Choose a reason for hiding this comment

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

@shubham1g5 Yes, this is potentially a regression rather than an existing issue.

In the previous Fragment implementation with setRetainInstance(true), the Android framework had special handling to manage view references during the fragment lifecycle. Even though your fragment was retained across configuration changes, the framework would properly detach views when needed.

ViewModels, however, don't have this same automatic cleanup mechanism. When a ViewModel holds direct references to Views like mInternalDetailView and tile, those views keep a reference to their Context (your Activity), preventing garbage collection.

For a minimal fix approach:

public class BreadcrumbBarViewModel extends ViewModel {
    // Instead of View references, store only the data needed
    private TreeReference tileReference;
    private String inlineDetailId;
    
    // Other minimal state needed to reconstruct the views
    private boolean isExpanded = false;
    
    // Remove the view field
    // private TabbedDetailView mInternalDetailView = null;
    // private View tile;
    
    // Getters/setters for the state
    public void setTileReference(TreeReference ref) {
        this.tileReference = ref;
    }
    
    public TreeReference getTileReference() {
        return tileReference;
    }
    
    // Similar methods for other state
}

Then in your Activity, maintain the view instances but use the ViewModel only for state. This keeps changes minimal while avoiding the leak.


✏️ Learnings added
Learnt from: shubham1g5
PR: dimagi/commcare-android#3042
File: app/src/org/commcare/fragments/BreadcrumbBarViewModel.java:50-55
Timestamp: 2025-04-21T15:02:17.464Z
Learning: ViewModels should not store View or Activity references as this can cause memory leaks. Unlike Fragments with setRetainInstance(true), ViewModels don't have automatic view detachment mechanisms. When migrating from Fragments to ViewModels, view references should be replaced with data-only state in the ViewModel.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would using weakreference fix it ?

Copy link

Choose a reason for hiding this comment

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

@shubham1g5 While using WeakReferences (WeakReference<View>) could technically reduce the likelihood of memory leaks, it's not the recommended approach for this issue for several reasons:

  1. Code complexity - You'd need to check for null every time you access the view: if (tileRef.get() != null) { ... }
  2. Reliability concerns - WeakReferences might still leak if there are other reference paths to the views
  3. Debugging difficulties - WeakReferences can make tracking object lifecycles more complicated
  4. Architectural mismatch - It patches the symptom rather than addressing the core design issue

The cleaner solution remains storing only data in the ViewModel and moving view creation/manipulation to the Activity or Fragment. This follows the proper architectural pattern for ViewModels.

If you're looking for a minimal change approach, WeakReferences could work as a temporary solution, but you'd need to add null checks throughout the code wherever these views are accessed.


🧠 Learnings used
Learnt from: shubham1g5
PR: dimagi/commcare-android#3042
File: app/src/org/commcare/fragments/BreadcrumbBarViewModel.java:50-55
Timestamp: 2025-04-21T15:02:17.464Z
Learning: ViewModels should not store View or Activity references as this can cause memory leaks. Unlike Fragments with setRetainInstance(true), ViewModels don't have automatic view detachment mechanisms. When migrating from Fragments to ViewModels, view references should be replaced with data-only state in the ViewModel.

@shubham1g5
Copy link
Contributor Author

@damagatchi retest this please

2 similar comments
@shubham1g5
Copy link
Contributor Author

@damagatchi retest this please

@shubham1g5
Copy link
Contributor Author

@damagatchi retest this please

@shubham1g5 shubham1g5 force-pushed the getRidOfSetRetainInstance branch from 36be1f4 to 5c9a52c Compare April 22, 2025 04:57
@shubham1g5
Copy link
Contributor Author

@damagatchi retest this please

4 similar comments
@shubham1g5
Copy link
Contributor Author

@damagatchi retest this please

@shubham1g5
Copy link
Contributor Author

@damagatchi retest this please

@shubham1g5
Copy link
Contributor Author

@damagatchi retest this please

@shubham1g5
Copy link
Contributor Author

@damagatchi retest this please

@shubham1g5 shubham1g5 force-pushed the getRidOfSetRetainInstance branch 2 times, most recently from 52d0856 to 7aa103a Compare April 24, 2025 08:58
@shubham1g5 shubham1g5 force-pushed the getRidOfSetRetainInstance branch from 7aa103a to 1dfb8b8 Compare April 24, 2025 12:14
@shubham1g5 shubham1g5 marked this pull request as ready for review April 24, 2025 12:16
@shubham1g5 shubham1g5 added this to the 2.57 milestone Apr 25, 2025
@shubham1g5 shubham1g5 merged commit 9d45a29 into master Apr 29, 2025
7 of 15 checks passed
@shubham1g5 shubham1g5 deleted the getRidOfSetRetainInstance branch April 29, 2025 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants