Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Fixed a crash when multiple retained components are created with different keys #813

Merged
merged 1 commit into from
Nov 29, 2024

Conversation

arkivanov
Copy link
Owner

@arkivanov arkivanov commented Nov 27, 2024

Fixes: #807.

Summary by CodeRabbit

  • New Features

    • Enhanced error handling for retained components to prevent duplicate key registration.
    • Improved state management for retained components using a Boolean serializer.
  • Bug Fixes

    • Addressed issues related to lifecycle management and state retention during configuration changes.
  • Tests

    • Introduced comprehensive unit tests for retained components, covering lifecycle events, state management, and exception handling.
    • Added new tests to verify lifecycle behavior during configuration changes.
    • Renamed existing test class to better reflect its purpose.
  • Chores

    • Updated the version of the essenty library to 2.3.0 for improved functionality.
    • Enhanced state management capabilities in the TestOwner class.

Copy link

coderabbitai bot commented Nov 27, 2024

Walkthrough

The pull request introduces modifications to the RetainedComponent.kt file, enhancing error handling and state management for retained components in Android. Key updates include checks to prevent duplicate component registration and a shift from String to Boolean serialization for state markers. Additionally, a new test suite, RetainedComponentMultipleTest.kt, is added to validate the functionality of retained components under various scenarios, while the existing test class is renamed to RetainedComponentSingleTest. The essenty library version is also updated from 2.2.1 to 2.3.0.

Changes

File Path Change Summary
decompose/src/androidMain/kotlin/com/arkivanov/decompose/RetainedComponent.kt Updated retainedComponent functions to improve error handling and state management logic.
decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt Added unit tests for RetainedComponent functionality, covering lifecycle management and state handling.
decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentSingleTest.kt Renamed test class from RetainedComponentTest to RetainedComponentSingleTest and added new tests.
deps.versions.toml Updated essenty library version from 2.2.1 to 2.3.0.
decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/TestOwner.kt Added isChangingConfigurations property and updated recreate method to enhance state management.

Assessment against linked issues

Objective Addressed Explanation
Ensure unique keys for retainedComponent instances to prevent crashes (#807)

Possibly related PRs

Poem

In the realm of code, a rabbit hops,
Fixing bugs and making stops.
With keys unique, no crashes near,
Retained components, have no fear!
A test suite blooms, with cases bright,
Hopping through code, from day to night! 🐇✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ 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 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: 1

🧹 Outside diff range and nitpick comments (3)
decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt (3)

46-60: Refactor Repeated Test Setup to Improve Maintainability

The test methods WHEN_recreated_THEN_old_lifecycle_destroyed and WHEN_recreated_THEN_new_lifecycle_resumed contain similar setup code involving the creation of TestOwner instances, retained components, and event logging. Consider extracting common setup steps into a helper method or utilizing a setup function annotated with @Before to reduce duplication and enhance readability.

Also applies to: 68-76


227-289: Consolidate Back Callback Registration in Back Handler Tests

The back handler tests (GIVEN_enabled_BackCallback_registered_in_all_retained_components_WHEN_onBackPressed_THEN_last_callback_called and others) have repetitive code for registering BackCallback instances and initializing flags. Refactoring this repeated code into a helper function or using parameterized tests could make the code cleaner and easier to maintain.


291-305: Clarify Default Parameter Usage in Helper Function

The retainedComponent helper function uses default values for parameters like key, discardSavedState, isStateSavingAllowed, and isChangingConfigurations. While defaults can simplify calls, explicitly specifying these parameters in test methods can improve readability and prevent unintended behaviors due to implicit values.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 0e2b83a and c2730fe.

📒 Files selected for processing (4)
  • decompose/src/androidMain/kotlin/com/arkivanov/decompose/RetainedComponent.kt (1 hunks)
  • decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt (1 hunks)
  • decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentSingleTest.kt (1 hunks)
  • deps.versions.toml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentSingleTest.kt
🔇 Additional comments (6)
deps.versions.toml (1)

5-5: Verify the essenty library update and its changes

The update from 2.2.1 to 2.3.0 might be related to the component retention fixes. Let's verify the changes and compatibility.

Consider documenting the reason for this version update in the PR description, especially if it's related to the component retention fixes.

✅ Verification successful

Based on the release information from the essenty library's version 2.3.0, I can now generate the final response:

The essenty library update is directly related to the PR's objective

The version 2.3.0 of essenty specifically adds a key parameter to StateKeeper Android extensions (#182), which aligns perfectly with the PR's goal of implementing unique keys for each retainedComponent to fix the crash when multiple retained components are created with the same key.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check the essenty library's changelog and release information

# Fetch release information for version 2.3.0
gh api repos/arkivanov/Essenty/releases | jq '.[] | select(.tag_name=="2.3.0") | {tag_name, body}'

# Check for any breaking changes or migration guides in docs
fd -e md -e txt . | xargs rg -i "migration|upgrade|breaking.?changes" 2>/dev/null

# Look for any existing usages of essenty that might need updates
rg "import.*essenty" 

Length of output: 50660

decompose/src/androidMain/kotlin/com/arkivanov/decompose/RetainedComponent.kt (3)

100-102: Prevent duplicate retained components with the same key

The added check ensures that an exception is thrown if another retained component is already registered with the same key, effectively preventing key collisions.


105-106: Proper initialization of StateKeeper and InstanceKeeper

Initializing stateKeeper and instanceKeeper with the appropriate parameters enhances state management and ensures consistency within the component lifecycle.


111-113: Ensure proper cleanup of discarded retained instances

Removing and destroying the retained instance when discardRetainedInstance is true prevents potential memory leaks and ensures that outdated instances do not persist.

decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt (2)

22-30: Test Correctly Validates Exception Handling for Duplicate Keys

The test WHEN_same_key_reused_THEN_throws_IllegalStateException effectively checks that reusing the same key throws an IllegalStateException, ensuring key uniqueness is enforced.


1-305: Comprehensive Test Suite Enhances Code Reliability

The RetainedComponentMultipleTest class provides extensive coverage of retained component behaviors, including lifecycle events, state management, instance retention, and back button handling. The well-structured tests effectively ensure robustness and correctness of the retained component functionality.

@arkivanov arkivanov force-pushed the fix-multiple-retained-components branch from c2730fe to c36b9a1 Compare November 27, 2024 20:42
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: 1

🧹 Outside diff range and nitpick comments (4)
decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/TestOwner.kt (1)

25-26: Add KDoc to document the isChangingConfigurations property

The property simulates Android's Activity.isChangingConfigurations behavior, but this isn't immediately clear without documentation. Consider adding KDoc to explain its purpose and relationship to Android's lifecycle.

+    /**
+     * Simulates Android's Activity.isChangingConfigurations behavior.
+     * Used to determine if the recreation is due to configuration changes,
+     * affecting how retained components and ViewModels are handled.
+     */
     var isChangingConfigurations: Boolean = true
decompose/src/androidMain/kotlin/com/arkivanov/decompose/RetainedComponent.kt (1)

168-181: Consider adding test-only documentation

The methods are correctly annotated with @SuppressLint("VisibleForTests"), but consider adding documentation to clarify their test-only nature.

Add KDoc comments like this:

+    /**
+     * Test-only method for handling back navigation events.
+     * @hide
+     */
     @SuppressLint("VisibleForTests")
     override fun handleOnBackStarted(backEvent: BackEventCompat) {
decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt (2)

96-97: Use unique keys for StateKeeper and InstanceKeeper registrations

You are registering state and instances with the same key "key" in multiple retained components (ctx1 and ctx2). While each component has its own StateKeeper and InstanceKeeper, using identical keys can lead to confusion or potential issues if the isolation is not handled as expected. It's a good practice to use distinct keys within each component to ensure clarity and prevent any unforeseen conflicts.

Consider updating the keys to be unique per component:

// StateKeeper registrations in tests:
- ctx1.stateKeeper.register(key = "key") { "saved_state_1" }
+ ctx1.stateKeeper.register(key = "key1") { "saved_state_1" }

- ctx2.stateKeeper.register(key = "key") { "saved_state_2" }
+ ctx2.stateKeeper.register(key = "key2") { "saved_state_2" }

// Consuming the state:
- val restoredState1 = ctx1.stateKeeper.consume<String>(key = "key")
+ val restoredState1 = ctx1.stateKeeper.consume<String>(key = "key1")

- val restoredState2 = ctx2.stateKeeper.consume<String>(key = "key")
+ val restoredState2 = ctx2.stateKeeper.consume<String>(key = "key2")

// InstanceKeeper usage:
- val instance11 = ctx1.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+ val instance11 = ctx1.instanceKeeper.getOrCreate(key = "key1", factory = ::TestInstance)

- val instance12 = ctx2.instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance)
+ val instance12 = ctx2.instanceKeeper.getOrCreate(key = "key2", factory = ::TestInstance)

Also applies to: 126-127, 146-147, 180-181, 198-199


258-271: Clarify dependency on BackCallback registration order

In the test GIVEN_enabled_BackCallback_registered_in_all_retained_components_WHEN_onBackPressed_THEN_last_callback_called, the invocation of the back callbacks depends on the order in which they are registered—the last registered enabled callback is called. This implicit reliance on registration order could lead to confusion or maintenance issues in the future if additional callbacks are added.

Consider adding comments to clarify this behavior or explicitly defining the priority to make the test intentions clear:

+ // The last registered BackCallback takes precedence
  ctx1.backHandler.register(BackCallback { isCalled1 = true })
  ctx2.backHandler.register(BackCallback { isCalled2 = true })

Alternatively, if the order is significant to functionality, consider refactoring to manage callback priority explicitly.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between c2730fe and c36b9a1.

📒 Files selected for processing (5)
  • decompose/src/androidMain/kotlin/com/arkivanov/decompose/RetainedComponent.kt (3 hunks)
  • decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt (1 hunks)
  • decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentSingleTest.kt (5 hunks)
  • decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/TestOwner.kt (1 hunks)
  • deps.versions.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentSingleTest.kt
  • deps.versions.toml
🔇 Additional comments (4)
decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/TestOwner.kt (2)

33-33: LGTM! Well-structured implementation of configuration change simulation

The implementation correctly simulates Android's configuration change behavior by setting the flag before state saving and using it to determine ViewModelStore handling. This provides precise control for testing different recreation scenarios.


25-33: Verify test coverage for multiple retained components

The new configuration change simulation capabilities should be leveraged to ensure comprehensive testing of multiple retained components with different keys.

✅ Verification successful

Let me check the actual test implementations to verify the coverage.


Let me check the test coverage for different scenarios in RetainedComponentMultipleTest.kt to ensure comprehensive testing.


Based on the test coverage analysis, I can now provide the final response:

Test coverage for multiple retained components is comprehensive

The implementation includes thorough test coverage for multiple retained components with different scenarios:

  • Configuration changes with both isChangingConfigurations = true/false
  • Multiple components with different keys being retained and recreated
  • Lifecycle state verification for multiple components
  • State saving/restoration with different configurations
  • Edge cases like duplicate keys and state discarding
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if the new isChangingConfigurations property is being used in retained component tests

# Look for tests using isChangingConfigurations
echo "Checking usage in retained component tests..."
rg -l "isChangingConfigurations" --type kotlin | grep -i "test"

# Look for multiple retained component test scenarios
echo "Checking multiple retained component test coverage..."
ast-grep --pattern 'fun test$$$RetainedComponent$$$'

Length of output: 693


Script:

#!/bin/bash
# Check the implementation of retained component tests
echo "Checking RetainedComponentMultipleTest.kt..."
rg "class RetainedComponentMultipleTest" -A 10 decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt

echo "\nChecking test methods in RetainedComponentMultipleTest.kt..."
ast-grep --pattern 'fun test$$$($$$) {
  $$$
}'  decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt

echo "\nChecking isChangingConfigurations usage in tests..."
rg "isChangingConfigurations" -B 2 -A 2 decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt

Length of output: 4212


Script:

#!/bin/bash
# Get all test method names and their context
echo "Checking all test methods in RetainedComponentMultipleTest.kt..."
rg "fun test" -B 1 -A 1 decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt

# Check for lifecycle testing
echo "\nChecking lifecycle-related test coverage..."
rg "lifecycle" -B 1 -A 1 decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt

# Check for state keeper testing
echo "\nChecking state keeper test coverage..."
rg "stateKeeper" -B 1 -A 1 decompose/src/androidUnitTest/kotlin/com/arkivanov/decompose/RetainedComponentMultipleTest.kt

Length of output: 3698

decompose/src/androidMain/kotlin/com/arkivanov/decompose/RetainedComponent.kt (2)

112-117: LGTM: Proper cleanup of discarded instances

The implementation correctly handles the cleanup sequence for discarded instances, ensuring proper lifecycle management and resource cleanup.


101-103: LGTM: Effective duplicate key prevention

The new check prevents crashes by detecting duplicate component registration early. This directly addresses the issue described in #807.

Let's verify this handles all duplicate key scenarios:

✅ Verification successful

Duplicate key check is comprehensive and correctly implemented

The verification shows that the duplicate key check in RetainedComponent.kt is the only place where savedStateRegistry.getSavedStateProvider is used for validation. The only other usage of the registry is in DeeplinkUtils.kt for a different purpose (deep link state registration). This confirms that the new check is the sole gatekeeper for preventing duplicate component registration.

The documentation in various navigation components (slots, stacks, panels, pages) consistently emphasizes unique key requirements, and this implementation aligns with that architectural requirement.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for other potential key validation or component registration patterns
# that might bypass this check

# Look for other component registration points
rg "savedStateRegistry\.getSavedStateProvider|savedStateRegistry\.registerSavedStateProvider" 

# Look for other key validation patterns
rg "check.*key|require.*key|assert.*key"

Length of output: 25777

@arkivanov arkivanov merged commit 8387e01 into master Nov 29, 2024
3 checks passed
@arkivanov arkivanov deleted the fix-multiple-retained-components branch November 29, 2024 10:58
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Multiple retainedComponent (RootComponent) ComponentContext for android app will crash
1 participant