Skip to content

Fix the High findings of the architecture review (#44 to #51) - #66

Merged
zeevy merged 8 commits into
mainfrom
fix/43-review-fixes
Oct 13, 2026
Merged

zeevy merged 8 commits into
mainfrom
fix/43-review-fixes

Conversation

@zeevy

@zeevy zeevy commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Fixes the 8 High findings from the architecture review in #43. One commit per issue.

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 trailing onDismiss lambda still compile. ToastData.offsetX and offsetY are deprecated.

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
@zeevy
zeevy merged commit 80d6656 into main Oct 13, 2026
4 checks passed
@zeevy zeevy mentioned this pull request Oct 13, 2026
22 tasks done
@zeevy
zeevy deleted the fix/43-review-fixes branch October 13, 2026 01:59
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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants