Kgothatso Ngako b95d723edd fix(chat): a tablet's room list can pick a second room, not just the first
The home screen's detail pane answered with the first room it was ever given. Tap
another and the list highlighted nothing, the transcript did not change, and the app
bar kept the first room's title -- so from the expanded breakpoint up, which is a
tablet in landscape and every desktop window, the split was a one-shot: the first
conversation you opened was the only one you could read without leaving the screen.

**`key(chatRoomId)` does not rebuild a view model, and the comment claiming it did was
the bug.** `viewModel()` resolves against `LocalViewModelStoreOwner.current` under a
key derived from the *class* -- `ViewModelProvider.get(modelClass)`, no key, straight
to the default `DEFAULT_KEY:canonicalName`. A composition key replaces a group:
`remember`s are forgotten and `LaunchedEffect`s restart, which is real but is not where
a view model lives. The store belongs to `HomeRoute`'s `NavBackStackEntry` and does not
change while the reader is on the home screen, so the second tap re-ran the
composition, asked for a `ChatRoomMessagingViewModel`, and got back the one holding
room one -- its `chatRoomId` a constructor val, its factory never invoked a second
time. `initiateChatRoomDetail()` then re-read room one and the pane redrew what it
already had.

**This is a list-detail-only failure, which is why nothing caught it.** Every other
screen in the app reaches the chat transcript by `navController.navigate(...)`, and a
pushed route is its own `NavBackStackEntry` and so its own store -- one subject per
owner, the class key never collides. The pane is the one place one screen is composed
for a succession of subjects without the entry changing. Below expanded the home screen
still pushes, so a phone never had this; the four existing cases in
`ChatPaneLayoutJvmTest` assert the widths and the empty state, which were right.

**`ViewModelStoreScopedTo` gives the pane a store of its own rather than keying the two
`viewModel()` calls by room id.** Both put the right room on screen. Keying would also
leave every room visited holding a live view model in the home entry's store until the
reader left the screen, each with its relay subscriptions and database collectors still
running -- a leak that grows with how much a reader browses, which on this layout is
the point of the layout. A scope that clears on the way out is the same correctness and
none of that.

**It is hand-rolled because the stock API is not here.** `rememberViewModelStoreOwner`
lands in `androidx.lifecycle:lifecycle-viewmodel-compose` 2.11.0 and does exactly this,
including surviving an Android configuration change by consulting the parent's
lifecycle; the build resolves `org.jetbrains.androidx.lifecycle`, and the class is in
neither the 2.9.6 nor the 2.11.0 artifact in the cache. A bare `ViewModelStoreOwner` is
the whole contract a scope needs here -- nothing in this app puts a `SavedStateHandle`
in a view model, so there is no saved state to delegate, and it deliberately does not
implement `HasDefaultViewModelProviderFactory` because every call inside passes its own
factory. The cost of hand-rolling is that a rotation rebuilds the pane's view models
instead of keeping them; the selection itself is `rememberSaveable` and survives, so
what that costs is a re-read of one room.

**Both wrappers stay, and they are not redundant.** The store scope alone would leave
the composer's draft text in place as the reader moved to the next room -- which on this
screen is the failure the composer is built around three separate signals to prevent,
one person's words going to another -- and would not restart the effect that loads a
room, so the pane would sit on `Loading`. `key` alone was the bug. One resets the
composition, the other the view models.

**Tests: a fifth case in `ChatPaneLayoutJvmTest`, checked against the bug reinstated.**
Two rooms told apart by their subject, which both panes draw as a title, so two nodes
saying it means "this is the room on the right" and one means "this room is merely
listed" -- an assertion that needs no new test tag and no reach into the view model.
Tap room one, wait for two; tap room two, wait for two of *it* and one of room one.
With `ViewModelStoreScopedTo` reduced to `content()` the second wait fails with
`ComposeTimeoutException` while the other four cases still pass, so the case pins the
view model scope rather than the layout. The waits are generous, because what they wait
for is a database read on another dispatcher and the cost of being wrong about a thread
hop is a test that fails on a loaded machine.

**Not done.** Nobody has looked at this running on a tablet; the test proves the pane
follows the selection, which is not the same thing. The list pane also still draws no
selected state -- M3's list-detail expects the selected row to carry one, and its
absence is its own reason the pane read as unresponsive. Left out deliberately: it is a
separate change to a different file, and this one is the part that was broken.

974 jvm tests, 454 common tests, `m3Audit` meeting every budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 20:39:26 +02:00
2026-09-09 17:04:09 +02:00
2026-03-24 04:47:19 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00

This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).

  • /composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:

    • commonMain is for code that’s common for all targets.
    • Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, the iosMain folder would be the right place for such calls. Similarly, if you want to edit the Desktop (JVM) specific part, the jvmMain folder is the appropriate location.
  • /iosApp contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.

Build and Run Android Application

To build and run the development version of the Android app, use the run configuration from the run widget in your IDE’s toolbar or build it directly from the terminal:

  • on macOS/Linux
    ./gradlew :composeApp:assembleDebug
    
  • on Windows
    .\gradlew.bat :composeApp:assembleDebug
    

Build and Run Desktop (JVM) Application

To build and run the development version of the desktop app, use the run configuration from the run widget in your IDE’s toolbar or run it directly from the terminal:

  • on macOS/Linux
    ./gradlew :composeApp:run
    
  • on Windows
    .\gradlew.bat :composeApp:run
    

Build and Run iOS Application

To build and run the development version of the iOS app, use the run configuration from the run widget in your IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.


Learn more about Kotlin Multiplatform…

Description
Mantra as a kotlin multiplatform project
https://mantra.press
Readme 13 MiB
Languages
Kotlin 100%