Skip to content

Fix attachments lost on activity recreation#6377

Merged
aleksandar-apostolov merged 2 commits intov6from
bug/AND-1145_fix_attachments_lost_on_activity_recreation
Apr 22, 2026
Merged

Fix attachments lost on activity recreation#6377
aleksandar-apostolov merged 2 commits intov6from
bug/AND-1145_fix_attachments_lost_on_activity_recreation

Conversation

@VelikovPetar
Copy link
Copy Markdown
Contributor

@VelikovPetar VelikovPetar commented Apr 17, 2026

Goal

Fix selected composer attachments being lost when the activity is destroyed and recreated (e.g., "Don't keep activities" developer option, process death). Previously, all in-memory composer state was lost on activity recreation since MessageComposerController stored everything in plain MutableStateFlow fields.

Implementation

Introduced a ComposerStateSaver abstraction that persists selected attachments via SavedStateHandle:

  • ComposerStateSaver — framework-agnostic interface the controller uses for save/restore (no Android imports in the controller)
  • SavedStateComposerStateSaverSavedStateHandle-backed implementation injected by the ViewModel factory
  • NoOpComposerStateSaver — fallback for the legacy create(modelClass) factory path (no persistence)
  • ParcelableAttachment@Parcelize wrapper containing only fields populated at compose time (upload, type, name, fileSize, mimeType, title, extraData)

Key design decisions:

  • Defensive parcel safety: Before saving, all extraData values are validated via isParcelSafe(). If any value is non-parcelable, saving is skipped entirely (falls back to current behavior — no crash, no silent data loss)
  • File existence check on restore: Attachments whose upload file no longer exists on disk are silently dropped
  • Attachments only: Text input is not persisted (handled by existing draft message mechanism). alsoSendToChannel is not persisted (only relevant in thread mode which isn't persisted)

UI Changes

No UI changes.

Testing

  1. Enable "Don't keep activities" in developer options
  2. Open a channel, select image/file attachments
  3. Open another activity on top (e.g., channel info)
  4. Return — verify attachments are still in the composer
  5. Send the message — verify it sends successfully with attachments
  6. Unit tests: ParcelableAttachmentTest, SavedStateComposerStateSaverTest, MessageComposerControllerTests

Summary by CodeRabbit

  • New Features
    • Message composer attachments are now persisted and automatically restored if the app is unexpectedly terminated while composing a message.

…teHandle

Co-Authored-By: Claude <noreply@anthropic.com>
@VelikovPetar VelikovPetar added the pr:bug Bug fix label Apr 17, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled.

🎉 Great job! This PR is ready for review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.26 MB 5.26 MB 0.00 MB 🟢
stream-chat-android-offline 5.49 MB 5.49 MB 0.00 MB 🟢
stream-chat-android-ui-components 10.64 MB 10.65 MB 0.00 MB 🟢
stream-chat-android-compose 12.87 MB 12.87 MB 0.00 MB 🟢

@VelikovPetar VelikovPetar marked this pull request as ready for review April 17, 2026 17:55
@VelikovPetar VelikovPetar requested a review from a team as a code owner April 17, 2026 17:55
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Walkthrough

This PR introduces message composer attachment state persistence across process death by implementing a ComposerStateSaver interface with concrete implementations (NoOpComposerStateSaver and SavedStateComposerStateSaver), integrating stateful persistence into MessageComposerController, and updating factory classes in compose and ui-components modules to leverage SavedStateHandle when available.

Changes

Cohort / File(s) Summary
Core State Persistence Interface
stream-chat-android-ui-common/src/main/kotlin/.../ComposerStateSaver.kt, NoOpComposerStateSaver.kt
New ComposerStateSaver interface defines contract for saving/restoring composer attachments. NoOpComposerStateSaver provides no-op fallback implementation.
Parcelable Attachment Support
stream-chat-android-ui-common/src/main/kotlin/.../ParcelableAttachment.kt, build.gradle.kts, api/stream-chat-android-ui-common.api
New @Parcelize data class ParcelableAttachment with conversion utilities and recursive parcel-safety validation. Adds kotlin-parcelize plugin. Updates API surface.
SavedState Persistence
stream-chat-android-ui-common/src/main/kotlin/.../SavedStateComposerStateSaver.kt
Implements ComposerStateSaver using androidx.lifecycle.SavedStateHandle to persist/restore attachments conditionally based on parcel-safety.
MessageComposerController Integration
stream-chat-android-ui-common/src/main/kotlin/.../MessageComposerController.kt
Accepts ComposerStateSaver dependency; restores attachments on init, observes changes to save state, and clears saved state on cleanup.
Compose Module Factory
stream-chat-android-compose/.../MessagesViewModelFactory.kt
Adds createMessageComposerViewModel(stateSaver) helper; parameterless create() uses NoOpComposerStateSaver; overloaded create(modelClass, extras) uses SavedStateComposerStateSaver when available.
UI Components Module Factory
stream-chat-android-ui-components/.../MessageListViewModelFactory.kt, api/stream-chat-android-ui-components.api
Parallels compose changes; adds create(modelClass, extras) overload and composer construction helper. Updates API surface.
Compose Module Tests
stream-chat-android-compose/.../MessageComposerViewModelTest.kt
Test fixture now explicitly passes NoOpComposerStateSaver into controller.
UI Common Module Tests
stream-chat-android-ui-common/src/test/.../MessageComposerControllerTests.kt, ParcelableAttachmentTest.kt, SavedStateComposerStateSaverTest.kt
Adds FakeComposerStateSaver test helper and three comprehensive test suites validating state restoration, clearing, parcelization round-trips, and safety checks.
UI Components Module Tests
stream-chat-android-ui-components/.../MessageComposerViewModelTest.kt
Test fixture now explicitly passes NoOpComposerStateSaver into controller.

Sequence Diagram(s)

sequenceDiagram
    participant Factory as ViewModel Factory
    participant Controller as MessageComposer<br/>Controller
    participant Saver as ComposerStateSaver
    participant Handle as SavedStateHandle
    participant UI as UI Layer

    Note over Factory,UI: Scenario 1: With CreationExtras
    Factory->>Handle: Extract SavedStateHandle from extras
    Factory->>Saver: Create SavedStateComposerStateSaver(handle)
    Factory->>Controller: Init with stateSaver
    
    Controller->>Saver: restoreAttachments()
    Saver->>Handle: Read saved parcelable attachments
    Handle-->>Saver: Return restored list or null
    Saver-->>Controller: List<Attachment>?
    Controller->>Controller: Set selectedAttachments if restored
    
    UI->>Controller: User selects attachment
    Controller->>Saver: saveAttachments(updated list)
    Saver->>Handle: Write parcelable to SavedStateHandle
    
    Note over Factory,UI: Scenario 2: Without CreationExtras
    Factory->>Saver: Create NoOpComposerStateSaver
    Factory->>Controller: Init with stateSaver
    Controller->>Saver: restoreAttachments()
    Saver-->>Controller: Always returns null
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

Through process rebirth, attachments survive,
Parceled and saved, forever alive,
The compose state hopper's work never lost,
Persistence achieved at minimal cost! 🐰📎✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix attachments lost on activity recreation' clearly and specifically describes the main change in the changeset.
Description check ✅ Passed The description covers Goal, Implementation, Testing, and provides comprehensive details about the changes and design decisions, though UI Changes section is marked 'No UI changes' and lacks visual verification materials.

✏️ 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 bug/AND-1145_fix_attachments_lost_on_activity_recreation

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt (1)

602-619: ⚠️ Potential issue | 🟡 Minor

Edit-mode attachments may be silently dropped after process death.

performMessageAction(Edit) assigns messageAction.message.attachments (remote attachments with no local upload file) to selectedAttachments. The save observer at Line 518-520 will then persist them via ParcelableAttachment, but on restore SavedStateComposerStateSaver drops any attachment whose upload file no longer exists — which is always true for remote edit-mode attachments. The user would lose them, and the edit flow itself is not covered by the draft-text mechanism either.

Consider either (a) skipping persistence when in edit mode, or (b) preserving non-upload attachments across restore (e.g., keep them without the file-existence gate). Not a blocker but worth a conscious decision.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt`
around lines 602 - 619, performMessageAction(Edit) places remote-only
attachments into selectedAttachments which are later saved, but
SavedStateComposerStateSaver drops attachments that lack a local upload.file on
restore; to fix, either (A) skip persisting attachments when the composer is in
edit flow by detecting the Edit action (performed in performMessageAction) or
the edit input source (MessageInput.Source.Edit) in the save observer and not
serializing selectedAttachments, or (B) change SavedStateComposerStateSaver to
preserve ParcelableAttachment instances that represent remote attachments (i.e.,
do not require upload.file existence) when restoring an edit session; update the
save/restore logic accordingly so performMessageAction(Edit),
selectedAttachments and the saver use the same edit-mode check and do not
silently drop remote attachments.
🧹 Nitpick comments (8)
stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/NoOpComposerStateSaver.kt (1)

22-28: Unresolved KDoc references.

The KDoc links [CreationExtras] and [ViewModelProvider.Factory.create] will not resolve since neither type is imported in this file (and they are Android framework types that this module-agnostic interface intentionally avoids). Consider downgrading them to plain backticks to avoid Dokka warnings.

✏️ Proposed tweak
- * Used as a fallback when the ViewModel is created without [CreationExtras]
- * (e.g. via the legacy [ViewModelProvider.Factory.create] overload).
+ * Used as a fallback when the ViewModel is created without `CreationExtras`
+ * (e.g. via the legacy `ViewModelProvider.Factory.create` overload).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/NoOpComposerStateSaver.kt`
around lines 22 - 28, The KDoc in NoOpComposerStateSaver.kt contains unresolved
link-style references to Android types; replace the bracketed links
[CreationExtras] and [ViewModelProvider.Factory.create] with plain code-style
references (`CreationExtras` and `ViewModelProvider.Factory.create`) in the doc
for ComposerStateSaver to avoid importing Android framework types and suppress
Dokka warnings.
stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTests.kt (2)

310-348: Consider factoring out the AppSettings fixture.

The AppSettings builder here is substantial and may be reused in future tests. Extracting a small helper (e.g., givenDefaultAppSettings() or a module-level fakeAppSettings()) would keep individual tests focused on the behavior under test. Not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTests.kt`
around lines 310 - 348, Factor out the large AppSettings construction into a
reusable test helper to reduce duplication: create a helper function (e.g.,
fakeAppSettings() or givenDefaultAppSettings()) that returns the AppSettings
instance used in tests, then replace the inline AppSettings block in the test
with givenAppSettings(fakeAppSettings()) or .givenDefaultAppSettings() in the
Fixture chain; update other tests accordingly to use the new helper and keep the
test focused on restoring attachments.

366-381: Strengthen the clearData assertion.

Currently only store.cleared is asserted. To defend against regressions where someone reorders clearData() so an empty-list save happens after clear() (effectively re-persisting empty state), also assert that store.restoreAttachments() returns null/empty after the call. Cheap guard, higher signal.

💡 Suggested addition
         controller.setMessageInput("some text")
         controller.clearData()
 
         assertTrue(store.cleared)
+        assertTrue(store.restoreAttachments().isNullOrEmpty())
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTests.kt`
around lines 366 - 381, The test `clearData clears the state store` currently
only asserts `store.cleared`; after calling `controller.clearData()` also assert
that saved attachments were removed by checking `store.restoreAttachments()` (or
equivalent restore method on `FakeComposerStateSaver`) returns null or an empty
list; update the test to call `store.restoreAttachments()` and assert it is
null/empty to prevent regressions where an empty save could re-persist state
after `clear()`.
stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ComposerStateSaver.kt (1)

22-47: Nullable vs empty-list semantics in restoreAttachments.

The return type is List<Attachment>? but the only caller treats null and empty identically (isNullOrEmpty()). If null vs empty is not meant to carry a different meaning, consider tightening the contract to List<Attachment> (returning emptyList() when nothing is persisted) and document the "no-op" case, or alternatively document in KDoc what each return value means. Low priority.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ComposerStateSaver.kt`
around lines 22 - 47, The restoreAttachments return type is nullable but callers
treat null the same as empty; update the ComposerStateSaver contract by changing
restoreAttachments(): List<Attachment>? to restoreAttachments():
List<Attachment> and ensure implementations return emptyList() when nothing is
persisted, or alternatively keep the nullable signature but add KDoc on
ComposerStateSaver.restoreAttachments explaining the semantic difference between
null and empty (and prefer the non-null empty-list approach); update
implementations of saveAttachments/restoreAttachments to comply and adjust any
callers if necessary (search for ComposerStateSaver.restoreAttachments usages to
confirm).
stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelFactory.kt (1)

143-145: Fallback path silently disables attachment persistence.

When a caller instantiates MessageComposerViewModel via the zero-extras create(modelClass) path, this factory returns a controller wired with NoOpComposerStateSaver, so attachments are not persisted across process death. In practice Android's ViewModelProvider will prefer create(modelClass, extras) when the owner supplies extras (Activity/Fragment with SavedStateRegistryOwner), so the common case is covered — but any custom ViewModelStoreOwner that doesn't expose saved state, or any direct factory.create(MessageComposerViewModel::class.java) call, silently loses persistence with no warning.

Consider a brief KDoc on the class (and/or a debug log in the NoOp branch) noting that persistence requires the extras-aware create overload, so integrators know they need a SavedStateRegistryOwner-backed owner.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelFactory.kt`
around lines 143 - 145, The factory's fallback path returns a controller using
NoOpComposerStateSaver which silently disables attachment persistence when
callers use the zero-extras create(modelClass) overload; update
MessageListViewModelFactory to clearly signal this by adding a short KDoc on the
class (or the create(modelClass) overload) stating that attachment persistence
requires using the extras-aware create(modelClass, extras) with a
SavedStateRegistryOwner, and add a debug/log message in the branch that
constructs the controller with NoOpComposerStateSaver (reference
NoOpComposerStateSaver, createMessageComposerViewModel, and
MessageComposerViewModel) to warn integrators when persistence is disabled.
stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ParcelableAttachmentTest.kt (3)

30-48: "Round-trip" tests don't actually marshal through a Parcel.

These tests only exercise the toParcelable()toAttachment() conversion functions; they never write/read the ParcelableAttachment to an actual android.os.Parcel. That means any issue introduced by @Parcelize code generation (e.g., an extraData value that passes isParcelSafe() but fails Parcel.writeValue, or type-coercion quirks like IntLong after readValue) will not be caught here — defeating the main reason this class is @Parcelize.

Consider adding a real Parcel round-trip covering at least round-trip preserves all fields and round-trip preserves extraData with primitives, e.g. under Robolectric:

val parcel = Parcel.obtain()
try {
    original.toParcelable().writeToParcel(parcel, 0)
    parcel.setDataPosition(0)
    val restored = ParcelableAttachment.CREATOR.createFromParcel(parcel).toAttachment()
    // assertions...
} finally {
    parcel.recycle()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ParcelableAttachmentTest.kt`
around lines 30 - 48, The tests currently only call toParcelable() →
toAttachment() and must be changed to perform a real Android Parcel round-trip
to validate `@Parcelize` behavior: for the test methods (e.g., `round-trip
preserves all fields` and `round-trip preserves extraData with primitives`)
obtain a Parcel, call original.toParcelable().writeToParcel(parcel, 0), reset
with parcel.setDataPosition(0), recreate via
ParcelableAttachment.CREATOR.createFromParcel(parcel).toAttachment(), assert
fields, and finally parcel.recycle(); update assertions to use the recreated
attachment instead of direct toParcelable().toAttachment() so Parcel write/read
edge-cases are covered.

177-192: Consider adding coverage for nested unsafe values.

The current "unsafe" cases put a non-parcelable object directly as a top-level extraData value. Given that isParcelSafe() is described as recursively validating nested lists/maps, a test with an unsafe value nested inside a List or Map (e.g., "list" to listOf("ok", object {}) or "nested" to mapOf("k" to object {})) would guard against regressions in the recursive check.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ParcelableAttachmentTest.kt`
around lines 177 - 192, Add tests to cover nested unsafe values by creating
attachments whose extraData contains a non-parcelable object nested inside a
List or Map (e.g., "list" -> listOf("ok", object {}) and "nested" -> mapOf("k"
to object {})) and assert that attachments.areExtraDataParcelSafe() returns
false; place these new tests alongside the existing tests in
ParcelableAttachmentTest (use the same test naming style, e.g.,
`attachments_with_nested_non_parcelable_extraData_are_NOT_parcel_safe` and
`nested_unsafe_value_makes_entire_list_unsafe`) to validate the recursive
isParcelSafe behavior.

102-126: Use named arguments for clarity: InProgress(bytesUploaded = 100, totalBytes = 1000) instead of positional arguments.

The current parameter order is correct (100 bytes uploaded out of 1000 total), but named arguments improve readability and make intent explicit, especially for numeric values that are difficult to interpret positionally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ParcelableAttachmentTest.kt`
around lines 102 - 126, In the test function `fields not in ParcelableAttachment
are null or default on restore` in ParcelableAttachmentTest, replace the
positional constructor call `Attachment.UploadState.InProgress(100, 1000)` with
a named-argument form `Attachment.UploadState.InProgress(bytesUploaded = 100,
totalBytes = 1000)` to make the intent explicit; update the `original`
Attachment creation to use the named parameters for `uploadState` while keeping
all other fields unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ParcelableAttachment.kt`:
- Around line 77-82: The isParcelSafe() implementation is too restrictive;
replace its whitelist logic by probing actual Parcel.writeValue behavior: in
isParcelSafe() obtain a Parcel via Parcel.obtain(), attempt
parcel.writeValue(this) (and optional parcel.setDataPosition(0) /
parcel.readValue(null) to ensure round-trip), catch any Throwable and return
false on error, finally recycle the parcel; update references to isParcelSafe()
(used by SavedStateComposerStateSaver.saveAttachments()) so types that
Parcel.writeValue accepts (Parcelable, Serializable, CharSequence, etc.) are
preserved instead of being cleared.

---

Outside diff comments:
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt`:
- Around line 602-619: performMessageAction(Edit) places remote-only attachments
into selectedAttachments which are later saved, but SavedStateComposerStateSaver
drops attachments that lack a local upload.file on restore; to fix, either (A)
skip persisting attachments when the composer is in edit flow by detecting the
Edit action (performed in performMessageAction) or the edit input source
(MessageInput.Source.Edit) in the save observer and not serializing
selectedAttachments, or (B) change SavedStateComposerStateSaver to preserve
ParcelableAttachment instances that represent remote attachments (i.e., do not
require upload.file existence) when restoring an edit session; update the
save/restore logic accordingly so performMessageAction(Edit),
selectedAttachments and the saver use the same edit-mode check and do not
silently drop remote attachments.

---

Nitpick comments:
In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ComposerStateSaver.kt`:
- Around line 22-47: The restoreAttachments return type is nullable but callers
treat null the same as empty; update the ComposerStateSaver contract by changing
restoreAttachments(): List<Attachment>? to restoreAttachments():
List<Attachment> and ensure implementations return emptyList() when nothing is
persisted, or alternatively keep the nullable signature but add KDoc on
ComposerStateSaver.restoreAttachments explaining the semantic difference between
null and empty (and prefer the non-null empty-list approach); update
implementations of saveAttachments/restoreAttachments to comply and adjust any
callers if necessary (search for ComposerStateSaver.restoreAttachments usages to
confirm).

In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/NoOpComposerStateSaver.kt`:
- Around line 22-28: The KDoc in NoOpComposerStateSaver.kt contains unresolved
link-style references to Android types; replace the bracketed links
[CreationExtras] and [ViewModelProvider.Factory.create] with plain code-style
references (`CreationExtras` and `ViewModelProvider.Factory.create`) in the doc
for ComposerStateSaver to avoid importing Android framework types and suppress
Dokka warnings.

In
`@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ParcelableAttachmentTest.kt`:
- Around line 30-48: The tests currently only call toParcelable() →
toAttachment() and must be changed to perform a real Android Parcel round-trip
to validate `@Parcelize` behavior: for the test methods (e.g., `round-trip
preserves all fields` and `round-trip preserves extraData with primitives`)
obtain a Parcel, call original.toParcelable().writeToParcel(parcel, 0), reset
with parcel.setDataPosition(0), recreate via
ParcelableAttachment.CREATOR.createFromParcel(parcel).toAttachment(), assert
fields, and finally parcel.recycle(); update assertions to use the recreated
attachment instead of direct toParcelable().toAttachment() so Parcel write/read
edge-cases are covered.
- Around line 177-192: Add tests to cover nested unsafe values by creating
attachments whose extraData contains a non-parcelable object nested inside a
List or Map (e.g., "list" -> listOf("ok", object {}) and "nested" -> mapOf("k"
to object {})) and assert that attachments.areExtraDataParcelSafe() returns
false; place these new tests alongside the existing tests in
ParcelableAttachmentTest (use the same test naming style, e.g.,
`attachments_with_nested_non_parcelable_extraData_are_NOT_parcel_safe` and
`nested_unsafe_value_makes_entire_list_unsafe`) to validate the recursive
isParcelSafe behavior.
- Around line 102-126: In the test function `fields not in ParcelableAttachment
are null or default on restore` in ParcelableAttachmentTest, replace the
positional constructor call `Attachment.UploadState.InProgress(100, 1000)` with
a named-argument form `Attachment.UploadState.InProgress(bytesUploaded = 100,
totalBytes = 1000)` to make the intent explicit; update the `original`
Attachment creation to use the named parameters for `uploadState` while keeping
all other fields unchanged.

In
`@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTests.kt`:
- Around line 310-348: Factor out the large AppSettings construction into a
reusable test helper to reduce duplication: create a helper function (e.g.,
fakeAppSettings() or givenDefaultAppSettings()) that returns the AppSettings
instance used in tests, then replace the inline AppSettings block in the test
with givenAppSettings(fakeAppSettings()) or .givenDefaultAppSettings() in the
Fixture chain; update other tests accordingly to use the new helper and keep the
test focused on restoring attachments.
- Around line 366-381: The test `clearData clears the state store` currently
only asserts `store.cleared`; after calling `controller.clearData()` also assert
that saved attachments were removed by checking `store.restoreAttachments()` (or
equivalent restore method on `FakeComposerStateSaver`) returns null or an empty
list; update the test to call `store.restoreAttachments()` and assert it is
null/empty to prevent regressions where an empty save could re-persist state
after `clear()`.

In
`@stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelFactory.kt`:
- Around line 143-145: The factory's fallback path returns a controller using
NoOpComposerStateSaver which silently disables attachment persistence when
callers use the zero-extras create(modelClass) overload; update
MessageListViewModelFactory to clearly signal this by adding a short KDoc on the
class (or the create(modelClass) overload) stating that attachment persistence
requires using the extras-aware create(modelClass, extras) with a
SavedStateRegistryOwner, and add a debug/log message in the branch that
constructs the controller with NoOpComposerStateSaver (reference
NoOpComposerStateSaver, createMessageComposerViewModel, and
MessageComposerViewModel) to warn integrators when persistence is disabled.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ba3a3c80-77be-424f-9e5b-201467584579

📥 Commits

Reviewing files that changed from the base of the PR and between aa5a191 and 5e9010a.

📒 Files selected for processing (15)
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModelTest.kt
  • stream-chat-android-ui-common/api/stream-chat-android-ui-common.api
  • stream-chat-android-ui-common/build.gradle.kts
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ComposerStateSaver.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/NoOpComposerStateSaver.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ParcelableAttachment.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/SavedStateComposerStateSaver.kt
  • stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTests.kt
  • stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/ParcelableAttachmentTest.kt
  • stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/internal/SavedStateComposerStateSaverTest.kt
  • stream-chat-android-ui-components/api/stream-chat-android-ui-components.api
  • stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelFactory.kt
  • stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt

Copy link
Copy Markdown
Contributor

@aleksandar-apostolov aleksandar-apostolov left a comment

Choose a reason for hiding this comment

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

LGTM, posted 1 question.

@sonarqubecloud
Copy link
Copy Markdown

@aleksandar-apostolov aleksandar-apostolov merged commit d0c2f78 into v6 Apr 22, 2026
20 of 21 checks passed
@aleksandar-apostolov aleksandar-apostolov deleted the bug/AND-1145_fix_attachments_lost_on_activity_recreation branch April 22, 2026 14:17
@stream-public-bot stream-public-bot added the released Included in a release label Apr 24, 2026
@stream-public-bot
Copy link
Copy Markdown
Contributor

🚀 Available in v6.37.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:bug Bug fix released Included in a release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants