Repository navigation
Fix the High findings of the architecture review (#44 to #51) - #66
Merged
Merged
Conversation
Every auto overlay registers under the same default tag, and unregisterHost(tag) removed whatever was stored there. After a back press, or after a splash Activity finished, the closing Activity removed the host of the one on screen, so ToastStack.show() returned null with no log. - unregisterHost(tag, state) removes the entry only if it still holds that state. - ToastStackHost registers again on every ON_RESUME. - The fallback host is the most recently registered one, not a random key. - The first toast dropped for lack of a host logs one warning. dismiss() does not count as a drop. - The demo app turns off the auto overlay, since it places its own host. Fixes #44
The ViewTree owners were set on the inner ComposeView, but Compose looks for them from the direct child of android.R.id.content upward, which is the FrameLayout wrapper. An Activity that never called setContentView has no owner above it, so it crashed with "ViewTreeLifecycleOwner not found". - Set the owners on the wrapper. The wrapper stays, because ComponentActivity.setContent reuses child 0 of the content view when it is a ComposeView, and would take over a bare overlay. - The wrapper is now a plain FrameLayout. Its dispatchTouchEvent override only called super. - findViewWithTag now asks for a View, the type that carries the tag. - Tests for a missing content view, a later setContent, and touch pass-through on and off a toast. Fixes #45
When maxVisible was reached, show() queued the toast but still returned a handle, and every handle call looked only at the toasts on screen. For a queued toast, await() never returned, dismiss() did nothing, and withAction(), onShow(), onDismiss() and updateProgress() were lost. A user onDismiss that threw also stopped the queue, since it ran before the next toast was promoted. - One lookup checks the toasts on screen and then the queue. The handle updates and the onDismiss chaining go through it. - dismiss() also removes a queued toast and fires its onDismiss. - dismiss() and dismissAll() finish every state change before they call user callbacks. dismissAll() also fires the onDismiss of queued toasts, so await() returns. Fixes #46
The overlay state lived in the overlay's composition, so a configuration change (rotation, dark mode, locale, fold) wiped every toast, and destroy() never called onDismiss, so await() and showAndAwait() callers hung. - The overlay state lives in a ViewModel in each Activity's ViewModelStore, made with our own factory so an app's default factory is not asked for it. It is destroyed when the Activity finishes. - destroy() fires onDismiss(Programmatic) for every toast on screen and in the queue. - A new host does not replay the enter animation, haptic, sound or onShow for toasts an earlier host already showed. - A toast held by a finger when its card leaves the screen gets its timer back, so it still times out. - AutoOverlayTouchTest is now AutoOverlayComposeTest, since it also covers rotation. Fixes #47
The host paused timers once when it entered composition, when the list is usually empty, so with TalkBack already on every new toast started a normal timer and was gone before it could be read. A drag release also restarted the timer under TalkBack. - The state has a screen reader flag, set by the host from the AccessibilityManager. While it is on, new and promoted toasts start paused, and a touch release does not resume them. Timers continue when TalkBack turns off. - Every toast has a "Dismiss" accessibility action (reason CloseButton), so a TalkBack user can close a toast that has no close button. - README states the behaviour. Fixes #48
A background ToastStack.show() ran the whole enqueue on the caller thread while the timers changed the same plain maps on the main thread, and user callbacks ran on whatever thread caused them. - Every state change runs inline on the main thread, or is posted there from any other thread. Callbacks always run on main. - show() from a background thread returns its handle at once. A call on that handle waits for the posted add, and a duplicate merged into an existing card keeps a working handle. - destroy() also ends a toast whose add was still posted. - timerMutex is gone, since pause and resume now run on main. - The global defaults are @volatile. - README and KDoc state the rule. Fixes #49
The per-toast options were copied into many signatures and had drifted, so the global one-liner could not set priority, animation, haptic or sound, and six README samples did not compile. - ToastStack.show and showAndAwait take the full option set. New parameters come after hostTag, so positional calls and the trailing onDismiss lambda still work. - soundUri can be set on show() and the builder. The secondary action is set with handle.withSecondaryAction or the builder. - The builder gets priority, customIcon, soundUri and the secondary action. Typed string helpers and ViewModel helpers get onDismiss. - ToastData.offsetX and offsetY are deprecated. Nothing public sets them. - README and KDoc samples are fixed. Every README Kotlin sample is compiled in the test sources, and a test checks that the README and the samples file match and that the entry points take the same options (kotlin-reflect, test only). - The demo calls ToastStack.success from a background thread. Fixes #50
The published AAR needed compileSdk 37 and AGP 9.1 (Compose 1.12, lifecycle-runtime-compose 2.11, an unused core-ktx), and carried Java 21 bytecode and Kotlin 2.2 metadata, while README said compileSdk 36 and Kotlin 2.0. - Build against Compose BOM 2026.06.01, activity 1.10.1, lifecycle 2.10.0 and coroutines 1.9.0. Drop core-ktx from the library. - Compile at Kotlin language and API level 2.0 with stdlib 2.0.21, and Java 11 bytecode. - The AAR declares minCompileSdk 35 and AGP 8.6.0. - consumer-check is an app on those minimums (AGP 8.6, Kotlin 2.0, compileSdk 35). CI builds it against the library from Maven Local. - Dependabot takes only patch updates for the library's runtime dependencies. - README Requirements and CONTRIBUTING state the real minimums. Fixes #51
22 tasks done
zeevy
added a commit
that referenced
this pull request
Oct 13, 2026
* fix: keep the auto overlay host when another Activity closes Every auto overlay registers under the same default tag, and unregisterHost(tag) removed whatever was stored there. After a back press, or after a splash Activity finished, the closing Activity removed the host of the one on screen, so ToastStack.show() returned null with no log. - unregisterHost(tag, state) removes the entry only if it still holds that state. - ToastStackHost registers again on every ON_RESUME. - The fallback host is the most recently registered one, not a random key. - The first toast dropped for lack of a host logs one warning. dismiss() does not count as a drop. - The demo app turns off the auto overlay, since it places its own host. Fixes #44 * fix: auto overlay no longer crashes an Activity with no content view The ViewTree owners were set on the inner ComposeView, but Compose looks for them from the direct child of android.R.id.content upward, which is the FrameLayout wrapper. An Activity that never called setContentView has no owner above it, so it crashed with "ViewTreeLifecycleOwner not found". - Set the owners on the wrapper. The wrapper stays, because ComponentActivity.setContent reuses child 0 of the content view when it is a ComposeView, and would take over a bare overlay. - The wrapper is now a plain FrameLayout. Its dispatchTouchEvent override only called super. - findViewWithTag now asks for a View, the type that carries the tag. - Tests for a missing content view, a later setContent, and touch pass-through on and off a toast. Fixes #45 * fix: handle calls reach toasts that wait in the queue When maxVisible was reached, show() queued the toast but still returned a handle, and every handle call looked only at the toasts on screen. For a queued toast, await() never returned, dismiss() did nothing, and withAction(), onShow(), onDismiss() and updateProgress() were lost. A user onDismiss that threw also stopped the queue, since it ran before the next toast was promoted. - One lookup checks the toasts on screen and then the queue. The handle updates and the onDismiss chaining go through it. - dismiss() also removes a queued toast and fires its onDismiss. - dismiss() and dismissAll() finish every state change before they call user callbacks. dismissAll() also fires the onDismiss of queued toasts, so await() returns. Fixes #46 * fix: auto overlay toasts survive rotation, and destroy ends await The overlay state lived in the overlay's composition, so a configuration change (rotation, dark mode, locale, fold) wiped every toast, and destroy() never called onDismiss, so await() and showAndAwait() callers hung. - The overlay state lives in a ViewModel in each Activity's ViewModelStore, made with our own factory so an app's default factory is not asked for it. It is destroyed when the Activity finishes. - destroy() fires onDismiss(Programmatic) for every toast on screen and in the queue. - A new host does not replay the enter animation, haptic, sound or onShow for toasts an earlier host already showed. - A toast held by a finger when its card leaves the screen gets its timer back, so it still times out. - AutoOverlayTouchTest is now AutoOverlayComposeTest, since it also covers rotation. Fixes #47 * fix: TalkBack pause covers new and queued toasts The host paused timers once when it entered composition, when the list is usually empty, so with TalkBack already on every new toast started a normal timer and was gone before it could be read. A drag release also restarted the timer under TalkBack. - The state has a screen reader flag, set by the host from the AccessibilityManager. While it is on, new and promoted toasts start paused, and a touch release does not resume them. Timers continue when TalkBack turns off. - Every toast has a "Dismiss" accessibility action (reason CloseButton), so a TalkBack user can close a toast that has no close button. - README states the behaviour. Fixes #48 * fix: toast state is changed only on the main thread A background ToastStack.show() ran the whole enqueue on the caller thread while the timers changed the same plain maps on the main thread, and user callbacks ran on whatever thread caused them. - Every state change runs inline on the main thread, or is posted there from any other thread. Callbacks always run on main. - show() from a background thread returns its handle at once. A call on that handle waits for the posted add, and a duplicate merged into an existing card keeps a working handle. - destroy() also ends a toast whose add was still posted. - timerMutex is gone, since pause and resume now run on main. - The global defaults are @volatile. - README and KDoc state the rule. Fixes #49 * feat: one option set for every show entry point The per-toast options were copied into many signatures and had drifted, so the global one-liner could not set priority, animation, haptic or sound, and six README samples did not compile. - ToastStack.show and showAndAwait take the full option set. New parameters come after hostTag, so positional calls and the trailing onDismiss lambda still work. - soundUri can be set on show() and the builder. The secondary action is set with handle.withSecondaryAction or the builder. - The builder gets priority, customIcon, soundUri and the secondary action. Typed string helpers and ViewModel helpers get onDismiss. - ToastData.offsetX and offsetY are deprecated. Nothing public sets them. - README and KDoc samples are fixed. Every README Kotlin sample is compiled in the test sources, and a test checks that the README and the samples file match and that the entry points take the same options (kotlin-reflect, test only). - The demo calls ToastStack.success from a background thread. Fixes #50 * fix: lower the floor for apps that use the library The published AAR needed compileSdk 37 and AGP 9.1 (Compose 1.12, lifecycle-runtime-compose 2.11, an unused core-ktx), and carried Java 21 bytecode and Kotlin 2.2 metadata, while README said compileSdk 36 and Kotlin 2.0. - Build against Compose BOM 2026.06.01, activity 1.10.1, lifecycle 2.10.0 and coroutines 1.9.0. Drop core-ktx from the library. - Compile at Kotlin language and API level 2.0 with stdlib 2.0.21, and Java 11 bytecode. - The AAR declares minCompileSdk 35 and AGP 8.6.0. - consumer-check is an app on those minimums (AGP 8.6, Kotlin 2.0, compileSdk 35). CI builds it against the library from Maven Local. - Dependabot takes only patch updates for the library's runtime dependencies. - README Requirements and CONTRIBUTING state the real minimums. Fixes #51 ---------
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the 8 High findings from the architecture review in #43. One commit per issue.
setContentView.await,dismiss,withAction,updateProgress,onDismiss) reach toasts that wait in the queue.destroy()ends everyawait().ToastStack.show,showAndAwait, the builder andToastStackState.showtake the same options. README samples compile, and a test keeps them in line.consumer-check/builds an app on those minimums in CI.Tests: 287 unit tests pass (254 before). Each new test fails on the old code. On an emulator, a back press (#44) and a rotation (#47) were checked by hand.
Public API: only additions. New parameters come after
hostTag, so positional calls and the trailingonDismisslambda still compile.ToastData.offsetXandoffsetYare deprecated.