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>
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…