Skip to content

Fix the Medium and Low findings of the architecture review (#52 to #65) - #67

Merged
zeevy merged 19 commits into
mainfrom
fix/43-review-medium
Oct 13, 2026
Merged

zeevy merged 19 commits into
mainfrom
fix/43-review-medium

Conversation

@zeevy

@zeevy zeevy commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Fixes the Medium and Low findings of the architecture review in #43 (#52 to #65), and refreshes the README. One commit per issue, plus review follow-ups.

Tests: 318 unit tests pass (254 at the start of #43). consumer-check (AGP 8.6, Kotlin 2.0, compileSdk 35) builds against the published library.

Behavior and API changes are listed in CHANGELOG.md under Unreleased.

The host mapped TopStart to Alignment.TopEnd in RTL, but Compose
Start and End alignments already flip in RTL, so the cards were
mirrored twice and sat on the wrong side.

- Positions map straight to the Start and End alignments.
- The layout of each position (alignment, arrangement, slide
  direction, full width) lives in ToastPosition.kt as exhaustive
  when blocks with no else, so a new position must define them.
- A test checks the card side for the four corners in LTR and RTL.

Fixes #52
The urgent path evicted the oldest toast with removeSilently, which
promoted a queued toast into the freed slot before the urgent toast
was added, so the stack held maxVisible + 1 cards.

- The eviction no longer promotes from the queue; the urgent toast
  takes the slot.
- The evicted toast's onDismiss runs after the urgent toast is in, so
  a show() from that callback waits in the queue.

Fixes #53
styleForType always set titleColor and iconTint to the type's content
color, so after merging they were never null and a custom
contentColor changed only the message text.

- The type defaults leave titleColor and iconTint unset, so they fall
  back to the content color, including a global or per-toast one.

Fixes #54
Minor releases changed public JVM signatures with no check and no
changelog, and the public surface was wider than documented.

- PublicApiTest dumps the public API with the binary compatibility
  validator library and compares it with toaststack/api/toaststack.api.
  The validator's Gradle plugin and KGP's ABI validation do not work
  with AGP 9's built-in Kotlin, so the library runs in a unit test.
  -PupdateApiDump writes a new dump.
- CHANGELOG.md covers 1.1.0 to 1.3.0 and the unreleased #43 work.
- ToastStackInitializer is internal, and ToastData's constructor and
  copy() are internal.
- consumer-rules.pro is empty. The demo release build runs R8 in CI.
  It skips the Compose mapping file, which AGP 9.4 cannot fetch for
  its built-in Kotlin 2.2.10.
- ToastStackConfig, rememberToastStackState(config) and WithToastStack
  are deprecated.
- Compose runtime, ui, foundation-layout, animation-core and
  lifecycle-viewmodel are api dependencies, since their types appear
  in the public API.
- README.md and the API dump are test inputs, so a change to either
  reruns the tests.

Fixes #55
The library manifest added VIBRATE to every app, even when no toast
vibrates, and an app that removed it crashed with SecurityException
on the first haptic toast.

- Haptics use View.performHapticFeedback, which needs no permission
  and follows the system haptic setting: reject for errors and
  warnings, confirm for success (Android 11+, with older fallbacks),
  a key tap for the rest.
- The VIBRATE permission and the Vibrator code are removed.
- README and CHANGELOG describe the change.

Fixes #56
The screen reader prefixes, the close label, the Dismiss action and
"(xN)" were hard coded in English, and the decorative type icon had
its own label, so TalkBack could read an extra stop.

- The labels and a toaststack_repeat_count plural are string
  resources. An app overrides them by name in its own values-xx.
- The type icon has no label; the type is in the card's label.
- A test uses a fake "zz" locale in the debug source set to show an
  override. It is not part of the release AAR.
- README lists the resource names.

Fixes #57
The icon lost its label in the previous commit, so check it by a test
tag instead.

Refs #57
The host wrote its render list during composition, read the animator
scale on every recomposition, and prepared the toast sound on the
main thread with errors swallowed.

- The render list follows state.toasts in a snapshotFlow effect, so
  the host never writes state it is drawing.
- The animator scale is read once per host.
- The sound plays on Dispatchers.IO, and a failure is logged once.
- A test removes and adds the host with the same state and checks
  onShow fired once. The state keeps the shown ids since #47.

Fixes #58
The 8 toast defaults were listed in 9 places, and the state read the
singleton's fields through a followsGlobalDefaults flag.

- ToastDefaults is one internal immutable data class. Its default
  values are the only place each default is written; the public
  parameters read them from ToastDefaults.Standard.
- ToastStack keeps one @volatile defaults value that configure()
  replaces in one write.
- ToastStackState reads its defaults through defaults(): its own
  values, or ToastStack.defaults for the auto overlay. The 8 own*
  fields, the branching getters and the flag are gone.
- The default content padding is written once.

No behaviour change. All tests pass unchanged.

Fixes #59
- ToastStackDefaults.Shape is a RoundedCornerShape, so compose
  foundation is an api dependency too.
- CHANGELOG lists every signature change in 1.1.0, 1.2.0 and the
  unreleased work, and says that a positional onDismiss no longer
  compiles.
- The API dump and the README check read files with LF line ends, and
  .gitattributes keeps *.api at LF.
- The demo R8 comment says it only builds.

Refs #55
The bugs in the #43 review passed CI because of gaps in the tests.

- ToastStack.resetForTest() and a ToastStackReset rule, used by every
  test class that touches the singleton, so a failed test does not
  leak a host or a configure() value.
- ToastStackState reads time through an internal now(), a monotonic
  clock (SystemClock.uptimeMillis) instead of the wall clock. Tests
  set it to the test scheduler's time.
- New tests: the paused timer keeps the time left, the dedup window
  edge, the ViewModel helpers from a real ViewModel, swipe dismiss,
  action tap, and the string resource overloads. Touch pass-through,
  RTL, threading and the multi-Activity cases came with #44 to #52.
- Phase7Test is now ToastBuilderTest and GapFixTest is now
  ToastQueueAndDedupTest. The assertTrue(true) test, the fake
  ViewModel test and a test that checked its own list are gone.

Fixes #60
- KDoc no longer says toasts are green, red, amber or blue (colors
  follow the color source since 1.1.0), that the oldest toast is
  evicted at the limit (normal toasts queue), that show() returns an
  id, or that timers pause when the app goes to the background.
- ToastData documents every property.
- The toast sound is held back in Do Not Disturb, as README and KDoc
  said, and uses notification audio attributes.

CLAUDE.md and CONTRIBUTING were corrected with #51, #55 and #60.

Fixes #61
- jitpack.yml uses JDK 21 Temurin and runs the same publish command as
  CI, so CI checks the path JitPack takes. CodeQL uses JDK 21 too.
- CI and the release workflow validate the Gradle wrapper first, and
  gradle-wrapper.properties pins the distribution checksum.
- Every action is pinned by commit SHA, with the tag in a comment, so
  Dependabot can keep them current.
- The release workflow fails when the tag does not match the version
  in the build, and attaches the AAR to the GitHub release.
- The library drops its unused instrumentation runner. (The demo's
  missing proguard-rules.pro was removed with #55.)

Fixes #62
Since the render list moved into an effect, a toast could be added
and then dismissed before the next frame. Its own effect still made
it visible and fired onShow, haptic and sound, and the card stayed on
screen with no way to close it.

- The per-toast effect returns at once when the toast is no longer in
  the state, so the exit runs and nothing is shown.
- ToastDefaults moves above the ToastStackState KDoc, which it had
  pushed off the class.

Refs #58 #59
- maxVisible below 1 fails fast in configure() and the ToastStackState
  constructor (rememberToastStackState and ToastStackConfig go through
  it), instead of sending every toast to the queue forever.
- A NaN progress counts as 0.
- The per-message time map is gone. Each card keeps its own last-shown
  time, which deduplication already looks up, so nothing is left once
  the card is dismissed.

Fixes #63
No behaviour change; all tests pass.

- ToastItem is split: the card, DefaultToastContent, one CloseButton
  (it was written twice), ActionButton, messageText and an internal
  accessibilityLabel. No composable is over about 110 lines.
- ToastStackHost is split: RegisterHost (registration and TalkBack),
  ToastSlot (one toast's effects, exit and card), and an internal
  syncRenderList with its own unit test.
- The timer engine (jobs, pause and remaining time, the clock and the
  scope) is a small internal ToastTimers class.
- StringResolver.resolve handles empty format arguments, so the five
  copies of that branch in ToastStackState are gone.

Fixes #64
The overlay went into every ComponentActivity, including third-party
login, payment and ad screens, with no way to leave one out short of
turning the feature off everywhere.

- ToastStack.configure(autoOverlay = { activity -> ... }) chooses
  which Activities get the overlay. Every ComponentActivity by default.
- A hand-placed host with the default tag in an Activity that also has
  the overlay logs one warning, since both would take toasts sent
  without a hostTag.
- README documents the filter; the API dump, CHANGELOG and samples
  are updated.

Fixes #65
The old images were crops of the demo app with its tabs showing
behind the toasts, and showed only the 1.0 colors.

- A Robolectric test (ReadmeScreenshots, skipped unless run with
  -PrecordScreenshots) renders clean scenes in light and dark with
  real shadows: the toast types in app theme colors, the library
  colors, actions and progress, positions, custom content, and an
  animated demo GIF.
- README shows them side by side, and its feature list now covers
  thread safety, the TalkBack pause, translations, rotation, leaving
  Activities out and the lower floor. It links the CHANGELOG.
- CONTRIBUTING says how to remake the images.
- Deduplication looks for a card with the message that is still in
  its window, so an older card out of its window no longer makes every
  repeat add a new card.
- configure() checks maxVisible first, so a bad value changes nothing.
- The dedup window test checks the window counts from the last
  repeat, and a test checks an Activity with only the overlay logs no
  shared tag warning.

Refs #60 #63 #65
@zeevy
zeevy merged commit c3c8e0b 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-medium branch October 13, 2026 01:59
zeevy added a commit that referenced this pull request Oct 13, 2026
… (#67)

* fix: RTL Start and End positions sit on the right side

The host mapped TopStart to Alignment.TopEnd in RTL, but Compose
Start and End alignments already flip in RTL, so the cards were
mirrored twice and sat on the wrong side.

- Positions map straight to the Start and End alignments.
- The layout of each position (alignment, arrangement, slide
  direction, full width) lives in ToastPosition.kt as exhaustive
  when blocks with no else, so a new position must define them.
- A test checks the card side for the four corners in LTR and RTL.

Fixes #52

* fix: an urgent toast keeps the stack at maxVisible

The urgent path evicted the oldest toast with removeSilently, which
promoted a queued toast into the freed slot before the urgent toast
was added, so the stack held maxVisible + 1 cards.

- The eviction no longer promotes from the queue; the urgent toast
  takes the slot.
- The evicted toast's onDismiss runs after the urgent toast is in, so
  a show() from that callback waits in the queue.

Fixes #53

* fix: a custom contentColor reaches the title and the icon

styleForType always set titleColor and iconTint to the type's content
color, so after merging they were never null and a custom
contentColor changed only the message text.

- The type defaults leave titleColor and iconTint unset, so they fall
  back to the content color, including a global or per-toast one.

Fixes #54

* feat: API dump check, CHANGELOG and a smaller public surface

Minor releases changed public JVM signatures with no check and no
changelog, and the public surface was wider than documented.

- PublicApiTest dumps the public API with the binary compatibility
  validator library and compares it with toaststack/api/toaststack.api.
  The validator's Gradle plugin and KGP's ABI validation do not work
  with AGP 9's built-in Kotlin, so the library runs in a unit test.
  -PupdateApiDump writes a new dump.
- CHANGELOG.md covers 1.1.0 to 1.3.0 and the unreleased #43 work.
- ToastStackInitializer is internal, and ToastData's constructor and
  copy() are internal.
- consumer-rules.pro is empty. The demo release build runs R8 in CI.
  It skips the Compose mapping file, which AGP 9.4 cannot fetch for
  its built-in Kotlin 2.2.10.
- ToastStackConfig, rememberToastStackState(config) and WithToastStack
  are deprecated.
- Compose runtime, ui, foundation-layout, animation-core and
  lifecycle-viewmodel are api dependencies, since their types appear
  in the public API.
- README.md and the API dump are test inputs, so a change to either
  reruns the tests.

Fixes #55

* fix: haptics without the VIBRATE permission

The library manifest added VIBRATE to every app, even when no toast
vibrates, and an app that removed it crashed with SecurityException
on the first haptic toast.

- Haptics use View.performHapticFeedback, which needs no permission
  and follows the system haptic setting: reject for errors and
  warnings, confirm for success (Android 11+, with older fallbacks),
  a key tap for the rest.
- The VIBRATE permission and the Vibrator code are removed.
- README and CHANGELOG describe the change.

Fixes #56

* fix: translatable accessibility labels and repeat count

The screen reader prefixes, the close label, the Dismiss action and
"(xN)" were hard coded in English, and the decorative type icon had
its own label, so TalkBack could read an extra stop.

- The labels and a toaststack_repeat_count plural are string
  resources. An app overrides them by name in its own values-xx.
- The type icon has no label; the type is in the card's label.
- A test uses a fake "zz" locale in the debug source set to show an
  override. It is not part of the release AAR.
- README lists the resource names.

Fixes #57

* test: the type icon is still drawn for each type

The icon lost its label in the previous commit, so check it by a test
tag instead.

Refs #57

* fix: host bookkeeping runs outside composition, sound off main

The host wrote its render list during composition, read the animator
scale on every recomposition, and prepared the toast sound on the
main thread with errors swallowed.

- The render list follows state.toasts in a snapshotFlow effect, so
  the host never writes state it is drawing.
- The animator scale is read once per host.
- The sound plays on Dispatchers.IO, and a failure is logged once.
- A test removes and adds the host with the same state and checks
  onShow fired once. The state keeps the shown ids since #47.

Fixes #58

* refactor: one defaults object for the global and the local paths

The 8 toast defaults were listed in 9 places, and the state read the
singleton's fields through a followsGlobalDefaults flag.

- ToastDefaults is one internal immutable data class. Its default
  values are the only place each default is written; the public
  parameters read them from ToastDefaults.Standard.
- ToastStack keeps one @volatile defaults value that configure()
  replaces in one write.
- ToastStackState reads its defaults through defaults(): its own
  values, or ToastStack.defaults for the auto overlay. The 8 own*
  fields, the branching getters and the flag are gone.
- The default content padding is written once.

No behaviour change. All tests pass unchanged.

Fixes #59

* fix: review follow-ups for the API check

- ToastStackDefaults.Shape is a RoundedCornerShape, so compose
  foundation is an api dependency too.
- CHANGELOG lists every signature change in 1.1.0, 1.2.0 and the
  unreleased work, and says that a positional onDismiss no longer
  compiles.
- The API dump and the README check read files with LF line ends, and
  .gitattributes keeps *.api at LF.
- The demo R8 comment says it only builds.

Refs #55

* test: shared singleton reset, injectable clock, missing tests

The bugs in the #43 review passed CI because of gaps in the tests.

- ToastStack.resetForTest() and a ToastStackReset rule, used by every
  test class that touches the singleton, so a failed test does not
  leak a host or a configure() value.
- ToastStackState reads time through an internal now(), a monotonic
  clock (SystemClock.uptimeMillis) instead of the wall clock. Tests
  set it to the test scheduler's time.
- New tests: the paused timer keeps the time left, the dedup window
  edge, the ViewModel helpers from a real ViewModel, swipe dismiss,
  action tap, and the string resource overloads. Touch pass-through,
  RTL, threading and the multi-Activity cases came with #44 to #52.
- Phase7Test is now ToastBuilderTest and GapFixTest is now
  ToastQueueAndDedupTest. The assertTrue(true) test, the fake
  ViewModel test and a test that checked its own list are gone.

Fixes #60

* docs: bring KDoc in line with the code, and honour Do Not Disturb

- KDoc no longer says toasts are green, red, amber or blue (colors
  follow the color source since 1.1.0), that the oldest toast is
  evicted at the limit (normal toasts queue), that show() returns an
  id, or that timers pause when the app goes to the background.
- ToastData documents every property.
- The toast sound is held back in Do Not Disturb, as README and KDoc
  said, and uses notification audio attributes.

CLAUDE.md and CONTRIBUTING were corrected with #51, #55 and #60.

Fixes #61

* ci: harden the release pipeline

- jitpack.yml uses JDK 21 Temurin and runs the same publish command as
  CI, so CI checks the path JitPack takes. CodeQL uses JDK 21 too.
- CI and the release workflow validate the Gradle wrapper first, and
  gradle-wrapper.properties pins the distribution checksum.
- Every action is pinned by commit SHA, with the tag in a comment, so
  Dependabot can keep them current.
- The release workflow fails when the tag does not match the version
  in the build, and attaches the AAR to the GitHub release.
- The library drops its unused instrumentation runner. (The demo's
  missing proguard-rules.pro was removed with #55.)

Fixes #62

* fix: a toast dismissed before its first frame leaves no card

Since the render list moved into an effect, a toast could be added
and then dismissed before the next frame. Its own effect still made
it visible and fired onShow, haptic and sound, and the card stayed on
screen with no way to close it.

- The per-toast effect returns at once when the toast is no longer in
  the state, so the exit runs and nothing is shown.
- ToastDefaults moves above the ToastStackState KDoc, which it had
  pushed off the class.

Refs #58 #59

* fix: input checks and a dedup state that does not grow

- maxVisible below 1 fails fast in configure() and the ToastStackState
  constructor (rememberToastStackState and ToastStackConfig go through
  it), instead of sending every toast to the queue forever.
- A NaN progress counts as 0.
- The per-message time map is gone. Each card keeps its own last-shown
  time, which deduplication already looks up, so nothing is left once
  the card is dismissed.

Fixes #63

* refactor: split the long composables and move the timers out

No behaviour change; all tests pass.

- ToastItem is split: the card, DefaultToastContent, one CloseButton
  (it was written twice), ActionButton, messageText and an internal
  accessibilityLabel. No composable is over about 110 lines.
- ToastStackHost is split: RegisterHost (registration and TalkBack),
  ToastSlot (one toast's effects, exit and card), and an internal
  syncRenderList with its own unit test.
- The timer engine (jobs, pause and remaining time, the clock and the
  scope) is a small internal ToastTimers class.
- StringResolver.resolve handles empty format arguments, so the five
  copies of that branch in ToastStackState are gone.

Fixes #64

* feat: let apps leave Activities out of the auto overlay

The overlay went into every ComponentActivity, including third-party
login, payment and ad screens, with no way to leave one out short of
turning the feature off everywhere.

- ToastStack.configure(autoOverlay = { activity -> ... }) chooses
  which Activities get the overlay. Every ComponentActivity by default.
- A hand-placed host with the default tag in an Activity that also has
  the overlay logs one warning, since both would take toasts sent
  without a hostTag.
- README documents the filter; the API dump, CHANGELOG and samples
  are updated.

Fixes #65

* docs: new README images and an up to date README

The old images were crops of the demo app with its tabs showing
behind the toasts, and showed only the 1.0 colors.

- A Robolectric test (ReadmeScreenshots, skipped unless run with
  -PrecordScreenshots) renders clean scenes in light and dark with
  real shadows: the toast types in app theme colors, the library
  colors, actions and progress, positions, custom content, and an
  animated demo GIF.
- README shows them side by side, and its feature list now covers
  thread safety, the TalkBack pause, translations, rotation, leaving
  Activities out and the lower floor. It links the CHANGELOG.
- CONTRIBUTING says how to remake the images.

* fix: review follow-ups for dedup and configure

- Deduplication looks for a card with the message that is still in
  its window, so an older card out of its window no longer makes every
  repeat add a new card.
- configure() checks maxVisible first, so a bad value changes nothing.
- The dedup window test checks the window counts from the last
  repeat, and a test checks an Activity with only the overlay logs no
  shared tag warning.

Refs #60 #63 #65

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