From 4330e02931c2124cd7d232d4e9a9386917eddf4a Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Wed, 9 Sep 2026 12:08:29 +0100 Subject: [PATCH 01/15] docs: record how Central's publishing limits constrain release cadence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Central enforces per-month file count, release size and release count from 1 October 2026. One toolbox release is 508 files — about half the monthly file allowance — because 26 modules each carry a full jar/sources/javadoc/pom/module set with signatures and checksums. That, not release count, is our binding constraint: a second release in the same calendar month barely fits and a third cannot. Two things worth writing down before someone reaches the wrong conclusion under time pressure. Same-month point releases need batching, since an August-style 1.8.0 -> 1.8.3 flurry would be over twice the allowance. And splitting the toolbox into separately-published repositories to shrink our footprint would do the opposite: Central scores a multi-module bundle as one release event, so 26 repositories would be 26 events per version, past the limit of 7 immediately. Cross-referenced from "Why one version for all modules", which recommended splitting a module out without noting that cost. Co-Authored-By: Claude Opus 5 (1M context) --- CONTRIBUTING.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 523fc7c..f0ed0c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,31 @@ one version; see [Why one version for all modules](#why-one-version-for-all-modu > **Releases are immutable.** A version can never be re-uploaded or corrected — the only remedy is > publishing a new one. Iterate with `--local` *before* releasing, never after. +### Central publishing limits — batch releases, do not split modules + +Maven Central enforces three per-calendar-month quotas per organisation, from 1 October 2026: file +count (~1,167), release size (78 MB) and release count (7). Track them in the +[Usage Center](https://central.sonatype.com/publishing/usage). + +One toolbox release is **508 files, 11.55 MB, and one release event** — Central scores a multi-module +deployment bundle as a single release, not one per artifact. So release count is a non-issue and size +is nowhere near. **File count is the binding constraint:** 508 files is roughly half the monthly +allowance, so a second release in the same calendar month lands at ~1,016 and a third cannot fit. + +Two consequences for release practice: + +- **Batch patch releases.** A flurry of same-month point releases — the 1.8.0 → 1.8.3 pattern of + August 2026 — would be ~2,540 files, over twice the allowance. Fold fixes into one version and + iterate through `--local` or a snapshot in the meantime. +- **Do not split modules to reduce usage; it does the opposite.** 26 separately-published + repositories would be 26 release events per version, past the limit of 7 on day one. The single + batched deployment is the cheapest possible shape under these rules — a further reason for the + caveat in [Why one version for all modules](#why-one-version-for-all-modules). + +Separately, Central's *commercial nature* classification is independent of publishing volume and can +require Publisher Pro on its own. Exemptions and limit adjustments for open-source group IDs are +requested from `central-support@sonatype.com`. + ### Credentials The script resolves credentials in two ways, in this order: @@ -142,7 +167,9 @@ coherent version set, so it cannot catch either. Republishing everything costs minutes of upload and no consumer risk. If a module ever genuinely earns its own release cadence, split it into its own repository rather than versioning it -independently here. +independently here — but weigh it against +[Central publishing limits](#central-publishing-limits--batch-releases-do-not-split-modules) first, +since each extra repository is another monthly release event. ## Documentation From 7f50c06abb0e9cf66f18a5779ccf516e5a8f003f Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Wed, 9 Sep 2026 12:08:29 +0100 Subject: [PATCH 02/15] ci: run tests when a draft PR is marked ready for review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Draft PRs cannot be merged, so the tests-and-coverage job now skips them rather than spending runner minutes. On its own that guard would silently strand any PR opened as a draft: `ready_for_review` is not in the default event type set, so marking such a PR ready fired no event at all and CI would never report on it. Adding the type alongside the guard is what makes the pairing safe. The `event_name` check keeps pushes to main running — on push there is no `event.pull_request`, and a null never equals false in GitHub expressions. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00c4ceb..b5b3bd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,10 @@ name: CI on: pull_request: branches: [ main ] + # `ready_for_review` is NOT in the default type set (opened/synchronize/reopened). + # Without it a PR opened as a draft and later marked ready fires no event at all, so + # the draft guard below would skip every run and CI would never report on that PR. + types: [ opened, synchronize, reopened, ready_for_review ] push: branches: [ main ] @@ -14,6 +18,11 @@ concurrency: jobs: test: name: Tests & coverage gate + # Draft PRs can't be merged, so don't spend runner minutes on them. Marking a PR ready + # fires `ready_for_review` (see types above), which is when CI runs for a drafted PR. + # The event_name check keeps pushes to main running: on push there is no + # `event.pull_request`, and a null never equals false in GitHub expressions. + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 8907d6560b682b7f8b6399519cda3b8c1f052d71 Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Wed, 9 Sep 2026 13:25:13 +0100 Subject: [PATCH 03/15] docs: correct the Central limits to the ones actually applied to us MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sonatype granted uk.co.appoly.droid an OSS exemption, so the commercial-nature classification — which applies regardless of publishing volume and would otherwise require Publisher Pro — does not apply to us. The same response declined to raise the file-count ceiling in substance. It was framed as "enhanced monthly publishing limits" of 7 releases / 80 MB / 1000 files, which is exactly what the Usage Center already showed before the request, sized to a publishing history of a single release. So the earlier note's numbers came from the published defaults rather than our real limits: the file ceiling is 1,000, not ~1,167, and the one-release-per-calendar-month conclusion stands. Recorded because that reply reads as a win on both counts and is easy to mistake for headroom we do not have. Co-Authored-By: Claude Opus 5 (1M context) --- CONTRIBUTING.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0ed0c2..3db6fe2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,9 +54,9 @@ one version; see [Why one version for all modules](#why-one-version-for-all-modu ### Central publishing limits — batch releases, do not split modules -Maven Central enforces three per-calendar-month quotas per organisation, from 1 October 2026: file -count (~1,167), release size (78 MB) and release count (7). Track them in the -[Usage Center](https://central.sonatype.com/publishing/usage). +Maven Central enforces three per-calendar-month quotas per organisation, from 1 October 2026. Our +applied limits, confirmed by Sonatype on 2026-09-09, are **1,000 files, 80 MB and 7 releases**. +Track usage in the [Usage Center](https://central.sonatype.com/publishing/usage). One toolbox release is **508 files, 11.55 MB, and one release event** — Central scores a multi-module deployment bundle as a single release, not one per artifact. So release count is a non-issue and size @@ -73,9 +73,12 @@ Two consequences for release practice: batched deployment is the cheapest possible shape under these rules — a further reason for the caveat in [Why one version for all modules](#why-one-version-for-all-modules). -Separately, Central's *commercial nature* classification is independent of publishing volume and can -require Publisher Pro on its own. Exemptions and limit adjustments for open-source group IDs are -requested from `central-support@sonatype.com`. +Sonatype granted `uk.co.appoly.droid` an **OSS exemption** on 2026-09-09, so Central's +*commercial nature* classification — which is independent of publishing volume and would otherwise +require Publisher Pro — does not apply to us. The same response declined to raise the file-count +ceiling in substance: the "enhanced" limits it granted match what was already applied, sized to a +publishing history of a single release. If the one-release-per-month cap starts to hurt, that is the +thing to go back to `central-support@sonatype.com` about, with a concrete cadence to justify it. ### Credentials From b8b38973f291f97f6bc2b52cbdce64dc7f3ed98b Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Wed, 9 Sep 2026 13:25:13 +0100 Subject: [PATCH 04/15] build: AGP 9.3.2 -> 9.4.0 Co-Authored-By: Claude Opus 5 (1M context) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8a3e677..4f1d3f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.3.2" +agp = "9.4.0" kotlin = "2.4.10" vanniktechPublish = "0.37.0" ksp = "2.3.11" From 91909ab3971bc456f3ca25abd6ba36ac311a2d7f Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Wed, 9 Sep 2026 13:25:13 +0100 Subject: [PATCH 05/15] build: Kotlin 2.4.10 -> 2.4.20 A patch-level move inside the same 2.4 language version, so the metadata version is unchanged and no consumer that resolves 1.9.0 today is affected. The consumer-visible kotlin-stdlib floor moves to 2.4.20, which the dependency graph was already forcing up from 2.1.21 and 2.2.21 transitives. Nothing else needed pinning: the Compose compiler and serialization plugins are version.ref'd to `kotlin` and moved in lockstep, and KSP2's versioning is decoupled from Kotlin, so the 2.3.11 pin still applies across the ten modules that use it plus Room in :app. Co-Authored-By: Claude Opus 5 (1M context) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f1d3f1..60762e4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "9.4.0" -kotlin = "2.4.10" +kotlin = "2.4.20" vanniktechPublish = "0.37.0" ksp = "2.3.11" coreKtx = "1.19.0" From 9ab405b0c964d06184efebda02ce0ce151b647a8 Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Wed, 9 Sep 2026 13:25:13 +0100 Subject: [PATCH 06/15] build: androidx Navigation 3 1.2.0-alpha07 -> 1.2.0-beta01 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version-only for production code. Every breaking change in beta01 and alpha07 is in the deep-link API — DeepLinkRequest.extras becoming RequestExtras, the factory function removals, DeepLinkMatcher gaining a type parameter — and this module imports no DeepLink* symbol. Two beta01 items did reach us. The new lint requiring Scene implementations to be data classes or implement equals/hashCode is already satisfied: TabsScene implements both explicitly. The contentKey change is the reason for the test edit below. NavEntry.contentKey now defaults to a composite of `key.toString()` and `key::class.toString()`, so the assertion pinning it to DetailScreen(5).toString() failed. Dropped rather than updated to the new format: the preceding assertion already compares contentKey to contentKey, and the backStack assertion above pins that the entry is DetailScreen(5), so identity stays covered without re-arming the same trap on the next release. Asserting on NavEntry.key instead is not an option — it is private in beta01. Co-Authored-By: Claude Opus 5 (1M context) --- .../java/uk/co/appoly/droid/nav3/TabsSceneStrategyTest.kt | 7 +++++-- gradle/libs.versions.toml | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Nav3Navigation/src/test/java/uk/co/appoly/droid/nav3/TabsSceneStrategyTest.kt b/Nav3Navigation/src/test/java/uk/co/appoly/droid/nav3/TabsSceneStrategyTest.kt index e33562b..5acd64b 100644 --- a/Nav3Navigation/src/test/java/uk/co/appoly/droid/nav3/TabsSceneStrategyTest.kt +++ b/Nav3Navigation/src/test/java/uk/co/appoly/droid/nav3/TabsSceneStrategyTest.kt @@ -69,9 +69,12 @@ class TabsSceneStrategyTest { val scene = calculate(entries) assertNotNull(scene) assertEquals(entries.dropLast(1), scene!!.previousEntries) - // dropLast(1) last entry is start tab top (DetailScreen(5)) + // dropLast(1) last entry is start tab top, which the backStack assertion above pins as + // DetailScreen(5). Identity is compared contentKey-to-contentKey rather than against a + // literal: Nav3 1.2.0-beta01 changed the default contentKey to a composite of + // `key.toString()` and `key::class.toString()`, and `NavEntry.key` is private, so pinning + // the format here would only re-arm this trap on the next release. assertEquals(entries[entries.lastIndex - 1].contentKey, scene.previousEntries.last().contentKey) - assertEquals(DetailScreen(5).toString(), scene.previousEntries.last().contentKey) assertEquals(listOf(entries.last()), scene.entries) assertEquals(1, entries.size - scene.previousEntries.size) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 60762e4..3696d3c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ sandwichVersion = "2.4.0" kotlinxSerialization = "1.11.0" paging = "3.5.1" roomVersion = "2.8.4" -nav3 = "1.2.0-alpha07" +nav3 = "1.2.0-beta01" workManager = "2.11.2" kover = "0.9.9" robolectric = "4.16.1" From 2bf651eeb5d8f1aacc1b6f5119457ad161434d72 Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Thu, 10 Sep 2026 12:19:14 +0100 Subject: [PATCH 07/15] docs(Nav3Navigation): stop recommending BackHandler for system back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Navigator API examples told consumers to wrap the host in a BackHandler that pops when `canPop` and otherwise switches tab. That duplicates `TabsNav3Navigator.pop()`, which `Nav3TabsHost` already wires into `NavDisplay.onBack`, and it intercepts the gesture before NavDisplay sees it, so `predictivePopTransitionSpec` never scrubs — losing the native predictive back this module exists to provide. It also hardcoded HomeTab where `startTab` is configurable and may sit mid-strip. Replaced with a "System back" section documenting the built-in path. Exiting the app needs no handler either: at the start-tab root `canPop` is false and Nav3 disables its back callback, so back falls through to the Activity even though retained tabs keep `backStack.size > 1` — asserted by Nav3PredictiveBackDeviceTest. Genuine per-screen interception now points at NavigationBackHandler from androidx.navigationevent, which arrives transitively via navigation3-ui, shares NavDisplay's dispatcher, and unlike BackHandler exposes gesture progress and cancellation. Also documents the API 33-35 `enableOnBackInvokedCallback` opt-in. It defaults true only on API 36+, and this module's minSdk is 23, so consumers below 36 were silently getting commit-only pops from a module whose headline feature is predictive back. Nothing in the repo mentioned it. The tabs docs are reframed so the per-tab stacks read as the source of truth and `backStack` as the derived projection NavDisplay renders from — which is what the code does, `tabStacks` being what every push/pop mutates. Leading with "flattened into a single backStack" invited the reading that the flat list is the model. A new "Why one NavDisplay" section records why one display rather than one per tab: Nav3 ties all per-entry state to back-stack membership via NavEntryDecorator.onPop and has no retained-but-off-stack concept, so a single display is what makes cross-tab retention possible at all, and it keeps predictive back working across a tab boundary (predictive back being per-NavDisplay) while avoiding a dispatcher per tab. Stale navigation3 version in Requirements corrected alpha07 -> beta01; UpdateReadmeVersions has no nav3 pattern, and that line is prose rather than a dependency block, so it does not self-heal. Co-Authored-By: Claude Opus 5 (1M context) --- Nav3Navigation/README.md | 103 +++++++++++++++--- .../uk/co/appoly/droid/nav3/Nav3Navigator.kt | 14 ++- .../co/appoly/droid/nav3/TabsNav3Navigator.kt | 46 ++++---- 3 files changed, 123 insertions(+), 40 deletions(-) diff --git a/Nav3Navigation/README.md b/Nav3Navigation/README.md index c99f4c2..12bfe1d 100644 --- a/Nav3Navigation/README.md +++ b/Nav3Navigation/README.md @@ -14,7 +14,7 @@ without giving up the fused-screen / ambient-navigator convenience that Voyager |--------------------------------------------|------------------------------------------------------------------------------------------------------| | `Nav3Screen` | Fused key + UI: implement `Content()` on the key class itself | | `Nav3Navigator` + `LocalNav3Navigator` | Ambient navigation: `push` / `pop` / `replace` / … + optional `parent` / `root()` / `currentOrThrow` | -| Stack peek | `canPop`, `lastItem`, `previousItem`, `items` — bottom bar, BackHandler, deep-link reconcile | +| Stack peek | `canPop`, `lastItem`, `previousItem`, `items` — bottom bar, back-enablement, deep-link reconcile | | `popWithResult` / `Nav3ResultReceiver` | Voyager-style screen-to-screen results (stable; preferred over the alpha result bus) | | `BackStackNav3Navigator` | Default navigator — navigation is list mutation on your `NavBackStack` | | `Nav3ScreenHost` | Full `NavDisplay` surface for `Nav3Screen` stacks + ambient navigator + default entry decorators | @@ -45,7 +45,16 @@ implementation("uk.co.appoly.droid:nav3navigation") **Requirements** - `minSdk` **23** (androidx.navigation3 requirement) -- Depends on `androidx.navigation3` **1.2.0-alpha07** (alpha result bus is optional; see [Results](#results)) +- Depends on `androidx.navigation3` **1.2.0-beta01** (alpha result bus is optional; see [Results](#results)) +- **Predictive back needs the manifest opt-in below API 36.** It defaults to `true` on API 36+, + but on API 33–35 the host app must set it explicitly, or pops commit with no gesture animation: + + ```xml + + ``` + + Never set it to `"false"` — that disables predictive back for the whole app, this module + included. - Screen classes need `kotlinx-serialization` (`@Serializable` + the serialization plugin) ## Usage @@ -138,13 +147,45 @@ navigator.popUntilRoot() // Bottom bar from top screen val showBottomBar = (navigator.lastItem as? ShowsBottomBar)?.showBottomBar != false +``` + +#### System back + +**Don't register a `BackHandler` for ordinary back.** `Nav3ScreenHost` forwards `onBack` to +`NavDisplay` (defaulting to `navigator.pop()`), and `Nav3TabsHost` defaults it to +`TabsNav3Navigator.pop()` — which already pops in-tab and falls back to exit-through-home at a +tab root. Registering a `BackHandler` above the host duplicates that logic *and* intercepts the +gesture before `NavDisplay` sees it, so `predictivePopTransitionSpec` never scrubs and you lose +the native predictive back this module exists to provide. + +Nor do you need one to exit the app: at the start-tab root `canPop` is `false` and Nav3 disables +its back callback, so back falls through to the Activity and finishes it as usual — even though +retained tabs keep `backStack.size > 1`. `Nav3PredictiveBackDeviceTest` asserts exactly this +(callback disabled at the start-tab root, enabled at any non-start tab root). + +To genuinely intercept back on one screen — an unsaved-changes prompt, say — use +`NavigationBackHandler` from `androidx.navigationevent:navigationevent-compose`, which is already +on your classpath transitively via `navigation3-ui`. It registers with the same dispatcher +`NavDisplay` uses and, being added later, is invoked first (handlers run last-in-first-out within +a priority); unlike `BackHandler` it also exposes the gesture's progress and cancellation: -// System back: pop tab stack, else switch tab / finish -BackHandler(enabled = navigator.canPop || currentTab != HomeTab) { - if (navigator.canPop) navigator.pop() else selectTab(HomeTab) +```kotlin +@Composable +override fun Content() { + val backState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None) + NavigationBackHandler( + state = backState, + isBackEnabled = hasUnsavedChanges, + onBackCompleted = { showDiscardDialog() }, + ) + // ...screen content } ``` +Bind one `NavigationEventState` to exactly one `NavigationBackHandler` — a second handler sharing +a state throws `IllegalArgumentException`. Branch inside `onBackCompleted` rather than registering +two conditional handlers. + ### Deep links A deep link is just a seeded start stack — no graph, no URI-pattern framework: @@ -175,11 +216,17 @@ semantics this module exists to provide. Nav3 only tears down per-entry state wh leaves the back stack; `TabsNav3Navigator` keeps every **visited** tab in `backStack`, and `Nav3TabsHost` defaults to `TabsSceneStrategy` so only the current tab’s top entry is rendered. +**Each tab really does own its own back stack.** Those per-tab stacks are the source of truth — +every `push` / `pop` / `replace` mutates exactly one of them. The single `backStack` you can read +is a *derived projection* of them, rebuilt on each mutation, because `NavDisplay` renders from one +list. The projection is a rendering adapter, not the data model — see +[Why one `NavDisplay`](#why-one-navdisplay) for what that buys. + Bottom-bar chrome stays **app-owned**. The library provides a navigator that: -- keeps **one stack per tab** and flattens **all visited tabs** into a single `backStack` for one - `NavDisplay` (`[other visited in tabOrder] + startTabStack + currentTabStack`), with the - current tab always the suffix +- keeps **one stack per tab** as the source of truth, and projects **all visited tabs** into a + single derived `backStack` for one `NavDisplay` + (`[other visited in tabOrder] + startTabStack + currentTabStack`), current tab always the suffix - pairs with **`TabsSceneStrategy`** (default on `Nav3TabsHost`) so inactive tabs stay in the stack without being composed — that is the retention mechanism - implements `Nav3Navigator` so in-tab `LocalNav3Navigator.push/pop` stay tab-local @@ -188,13 +235,13 @@ Bottom-bar chrome stays **app-owned**. The library provides a navigator that: - records **`pendingTabSlide`** so tab switches can animate directionally (see [Transitions](#transitions)) - exposes **`currentTabDepth`** (depth of the current tab only) for in-tab transition z-index — not `backStack.size`, which grows as tabs are visited -- **`items`** returns the **current tab’s** stack only (Voyager-equivalent), not the full multi-tab - flatten — use `stackFor(tab)` or `backStack` when you need another tab or the display list +- **`items`** returns the **current tab’s** stack only (Voyager-equivalent), not the full + multi-tab projection — use `stackFor(tab)` or `backStack` for another tab or the display list - separates **display order** (`tabOrder`) from the **launch / exit-through-home tab** (`startTab`) `tabOrder` is the strip order (bottom-bar left→right, and the indices used for `TabSlide.Forward` / `Backward`). `startTab` is the launch tab, the exit-through-home target, -and the stack always flattened underneath the current tab — it defaults to `tabOrder.first()` so +and the stack always projected underneath the current tab — it defaults to `tabOrder.first()` so existing call sites stay source-compatible, but can be any entry of `tabOrder` (e.g. a centre Home). ```kotlin @@ -348,7 +395,7 @@ same reflection-based `NavKey` serialization as `rememberNavBackStack`). Screens restore (not read from the saved bundle). If `KEY_CURRENT` is missing, restore falls back to the start tab's index — not `0`. -**Equal keys across tabs:** visited tabs share one flattened `backStack`. The same equal key on +**Equal keys across tabs:** visited tabs share one projected `backStack`. The same equal key on Home and on Rooms shares saveable state / ViewModelStore — use distinguishing constructor args when a destination can appear under more than one tab. @@ -360,11 +407,31 @@ composed, so no ViewModel is created until the tab is selected. `CompositionLocalProvider(LocalTabsNavigator provides tabs) { Nav3ScreenHost(...) }` yourself if you need a custom layout; pass `TabsSceneStrategy(tabs)` (or equivalent) if you want retention. -#### Multi-stack alternative +#### Why one `NavDisplay` + +Per-tab stacks are the model, but they are deliberately projected into **one** `NavDisplay` +rather than given a display each. Nav3 ties all per-entry state to back-stack membership — +`NavEntryDecorator`'s `onPop` fires when a key leaves the stack, and that is what clears an +entry's `rememberSaveable` state and `ViewModelStore`. There is no "retained but off-stack" +concept in the runtime. So one display is what makes retention possible at all, and it also: + +- **keeps predictive back working across a tab boundary.** Predictive back is per-`NavDisplay`, + so exit-through-home can only animate while both tabs' entries live in the same display's + stack (`Nav3PredictiveBackDeviceTest` covers this). +- **avoids competing back dispatchers.** One display means one `NavigationEvent` dispatcher. + A display per tab would need a child dispatcher owner scoped per tab, enabled only for the + selected one. +- **keeps inactive tabs out of composition** — retained, not composed, via `TabsSceneStrategy`. + +The cost is that stable tab-root keys never leave the stack, which is why retention needs an +explicit end — see [Retention and teardown](#retention-and-teardown) and call +`Nav3RetentionScope.clear()` on sign-out. -If you prefer independent `rememberNavBackStack` per tab (no flatten / no built-in tab-slide), -swap which stack you pass to `Nav3ScreenHost` and re-provide `LocalNav3Navigator` — same idea as -nested Voyager navigators. Cross-tab then means mutating the target tab's list yourself. +**Independent-stack alternative.** If you want a stack per tab with *no* cross-tab retention +(and no built-in tab-slide), swap which `rememberNavBackStack` you pass to `Nav3ScreenHost` and +re-provide `LocalNav3Navigator` — same idea as nested Voyager navigators. Cross-tab then means +mutating the target tab's list yourself, and switching tabs tears down the previous tab's +saveable state and ViewModels, since its keys leave the back stack. ### Transitions @@ -540,10 +607,10 @@ Drop `uniqueScreenKey` — multi-instance identity is the constructor args (and | `Nav3ResultReceiver` | interface | `onResult` target for `popWithResult` | | `popWithResult` / `popUntilWithResult` | extensions | Deliver result + pop | | `Nav3ScreenHost` | composable | Full `NavDisplay` host + ambient navigator | -| `TabsNav3Navigator` | class | Per-tab stacks retained in flatten + `startTab` + `navigateToTab` | +| `TabsNav3Navigator` | class | Per-tab stacks + derived projection + `navigateToTab` | | `TabsNav3Navigator.startTab` | property | Launch / exit-through-home tab (may sit mid-strip) | | `TabsNav3Navigator.currentTabDepth` | property | Depth of the current tab only (in-tab transition z-index) | -| `TabsNav3Navigator.items` | property | **Current tab’s** stack only (not the multi-tab flatten) | +| `TabsNav3Navigator.items` | property | **Current tab’s** stack only (not the multi-tab projection) | | `TabsNav3Navigator.exitToStartTabSlide` | property | Slide direction a committed exit-through-home `pop` would use | | `TabsSceneStrategy` | class | Renders current tab top; retains inactive tab state in back stack | | `LocalTabsNavigator` | CompositionLocal | Ambient tabs API (`null` outside a tab host) | diff --git a/Nav3Navigation/src/main/java/uk/co/appoly/droid/nav3/Nav3Navigator.kt b/Nav3Navigation/src/main/java/uk/co/appoly/droid/nav3/Nav3Navigator.kt index e26d5c2..e023ed5 100644 --- a/Nav3Navigation/src/main/java/uk/co/appoly/droid/nav3/Nav3Navigator.kt +++ b/Nav3Navigation/src/main/java/uk/co/appoly/droid/nav3/Nav3Navigator.kt @@ -140,12 +140,20 @@ interface Nav3Navigator { */ fun popUntilRoot() - // --- stack introspection (bottom bar, BackHandler, deep-link reconcile) --- + // --- stack introspection (bottom bar, back-enablement, deep-link reconcile) --- /** * `true` when there is a previous screen to pop to (stack size > 1) — Voyager's `canPop`. - * Use with system [androidx.activity.compose.BackHandler]: pop when `canPop`, otherwise - * switch tab / finish the activity. + * + * Read this for UI decisions (an up arrow, a back-enabled check). **Do not** drive system back + * from it via [androidx.activity.compose.BackHandler]: [Nav3ScreenHost] already routes + * `NavDisplay.onBack` to [pop], and a handler above the host intercepts the gesture before + * `NavDisplay` sees it, defeating the predictive-back scrub. When `canPop` is `false` Nav3 + * disables its back callback so the Activity finishes as usual. + * + * To intercept back on a single screen (e.g. an unsaved-changes prompt), use + * `NavigationBackHandler` from `androidx.navigationevent:navigationevent-compose` inside that + * screen's content — it shares `NavDisplay`'s dispatcher and keeps gesture progress. */ val canPop: Boolean diff --git a/Nav3Navigation/src/main/java/uk/co/appoly/droid/nav3/TabsNav3Navigator.kt b/Nav3Navigation/src/main/java/uk/co/appoly/droid/nav3/TabsNav3Navigator.kt index b6a8f5a..3b14ed8 100644 --- a/Nav3Navigation/src/main/java/uk/co/appoly/droid/nav3/TabsNav3Navigator.kt +++ b/Nav3Navigation/src/main/java/uk/co/appoly/droid/nav3/TabsNav3Navigator.kt @@ -41,17 +41,23 @@ enum class TabSlide { val LocalTabsNavigator = staticCompositionLocalOf { null } /** - * Per-tab back stacks flattened into a single [backStack] for one [Nav3ScreenHost] / - * [androidx.navigation3.ui.NavDisplay]. + * One back stack per tab, projected into the single [backStack] that one [Nav3ScreenHost] / + * [androidx.navigation3.ui.NavDisplay] renders from. * * ## Model * - * - Each tab has its own stack. [startTab] is the launch tab and **exit-through-home** target - * (defaults to the first entry of [tabOrder]; pass an explicit [startTab] when the home tab - * is not first in the strip, e.g. a centre Home among Stations · Kerbside · Home · …). - * - The display stack retains **every visited tab** so Nav3 keeps per-tab saveable state and - * ViewModelStores across tab switches (entries are only torn down when their key leaves the - * back stack). Flatten order: + * **The per-tab stacks are the source of truth.** Each tab owns its own stack, and every + * navigator operation ([push], [pop], [replace], [navigateToTab]) mutates exactly one tab's + * stack. [backStack] is a *derived projection* of those stacks, rebuilt after each mutation, + * because `NavDisplay` renders from a single list — it is a rendering adapter, not the model. + * + * - [startTab] is the launch tab and **exit-through-home** target (defaults to the first entry + * of [tabOrder]; pass an explicit [startTab] when the home tab is not first in the strip, + * e.g. a centre Home among Stations · Kerbside · Home · …). + * - The projection retains **every visited tab** so Nav3 keeps per-tab saveable state and + * ViewModelStores across tab switches. Nav3 clears an entry's state only when its key leaves + * the back stack, so stack membership is the only retention lever it offers. Projection + * order: * `[other visited tabs in tabOrder] + startTabStack + (currentTabStack if not start)`. * The current tab is always the suffix (`backStack.last()` is its top). Pair with * [TabsSceneStrategy] (the [Nav3TabsHost] default) so only the current top is rendered. @@ -73,7 +79,7 @@ val LocalTabsNavigator = staticCompositionLocalOf { null } * * ## Equal keys across tabs * - * Visited tabs share one flattened [backStack]. If the same equal key appears under more than + * Visited tabs share one projected [backStack]. If the same equal key appears under more than * one tab (e.g. `DetailScreen(1)` on Home and on Rooms), Nav3 treats them as the same entry for * saveable state / ViewModelStore — the same rule as duplicate keys on a single stack. Prefer * distinguishing constructor args when the same destination can live under more than one tab. @@ -101,7 +107,7 @@ val LocalTabsNavigator = staticCompositionLocalOf { null } * @param tabOrder tab roots in strip order (used for [TabSlide] direction and bottom-bar order). * Must be non-empty. Each root is kept as the first entry of that tab's stack and is never * popped or replaced. Only tabs in this list may be passed to [switchTab] / [navigateToTab]. - * @param startTab the launch tab, exit-through-home target, and stack always flattened + * @param startTab the launch tab, exit-through-home target, and stack always projected * underneath the current tab. Must be one of [tabOrder]. Defaults to the first entry of * [tabOrder] so existing call sites stay source-compatible. * @param parent the navigator that nested this tab shell (typically the root host), or `null` @@ -120,7 +126,7 @@ class TabsNav3Navigator( val tabOrder: List = tabOrder.toList() /** - * The launch tab, the exit-through-home target, and the stack always flattened underneath + * The launch tab, the exit-through-home target, and the stack always projected underneath * the current tab's stack. Independent of [tabOrder] index — may sit mid-strip. */ val startTab: Nav3Screen = startTab @@ -130,8 +136,10 @@ class TabsNav3Navigator( ) /** - * Flattened stack for [Nav3ScreenHost] / [androidx.navigation3.ui.NavDisplay]. - * Mutated only via this navigator — do not edit directly. + * The per-tab stacks projected into the single list [Nav3ScreenHost] / + * [androidx.navigation3.ui.NavDisplay] renders from. **Derived state**, rebuilt on every + * mutation — the per-tab stacks are the source of truth, so mutate only via this navigator + * and never edit this list directly. */ val backStack: NavBackStack = NavBackStack(this.startTab) @@ -326,18 +334,18 @@ class TabsNav3Navigator( /** * Top of the current tab's stack (always `backStack.last()`, since the current tab is the - * flattened suffix). + * projected suffix). */ override val lastItem: Nav3Screen? get() = backStack.lastOrNull() as? Nav3Screen /** - * Entry immediately beneath the current top in the flattened [backStack]. + * Entry immediately beneath the current top in the projected [backStack]. * * - Deeper in a tab → that tab's previous screen. * - At a non-start tab root → the top of [startTab]'s stack (what exit-through-home reveals). * - At the start-tab root with retained visited tabs → another tab's entry may sit beneath - * home in the flatten; [canPop] is still `false` and [TabsSceneStrategy] reports empty + * home in the projection; [canPop] is still `false` and [TabsSceneStrategy] reports empty * `previousEntries`, so system back backgrounds the app rather than navigating there. */ override val previousItem: Nav3Screen? @@ -345,11 +353,11 @@ class TabsNav3Navigator( /** * Screens on the **current tab's** stack only (root first) — Voyager-equivalent meaning of - * “the stack”, not the full flattened multi-tab [backStack]. + * “the stack”, not the full projected multi-tab [backStack]. * * Deliberate behaviour: callers inspecting `items` for bottom-bar chrome, deep-link * reconcile, or “am I on X?” want the active tab, not every retained visited tab. - * Use [stackFor] or [backStack] when you need another tab or the display flatten. + * Use [stackFor] or [backStack] when you need another tab or the display projection. */ override val items: List get() = stackFor(currentTab) @@ -417,7 +425,7 @@ class TabsNav3Navigator( } /** - * Rebuilds the flattened [backStack] so every **visited** tab keeps its entries (Nav3 only + * Rebuilds the projected [backStack] so every **visited** tab keeps its entries (Nav3 only * tears down saveable / ViewModel state when a content key leaves the back stack). * * Order: other visited tabs in [tabOrder] (excluding [startTab] and [currentTab]) + From fdd3bd9fc3db5f98501949ebcdc508c773db3196 Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Thu, 10 Sep 2026 12:19:57 +0100 Subject: [PATCH 08/15] build: androidx Navigation 3 1.2.0-beta01 -> 1.2.0-rc01 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug fixes only. A full source diff of both artifacts between beta01 and rc01 turns up exactly two changed files, and no public API change at all — expected, since rc freezes the API. NavEntry's `defaultContentKey` moved from `Pair("$key", "${key::class}")` to `"$key:${key::class}"`. It is @PublishedApi internal but used as a default constructor argument, so it compiles into NavEntry's own synthetic rather than inlining into consumers, and there is no binary-compatibility exposure for a library that ships against one version while its consumers compile against another. TabsSceneStrategyTest is unaffected: 9ab405b already rewrote those assertions to compare contentKey to contentKey precisely because beta01 churned this field once before, so the trap was disarmed ahead of time. UriDeepLinkMatcher gained duplicate-placeholder validation and a ParsedPattern refactor. Unreachable here — this module imports no DeepLink* symbol, deep links being a seeded start stack rather than a URI-pattern framework. Verified on rc01: the module's 120 JVM tests, koverVerify, and :app:verifyConsumerKeepRules, plus the full 14-test on-device suite, which is the part that matters — it covers real predictive-back gestures and Activity recreation, and `aFreshLaunchDoesNotInheritThePreviousActivitysTabViewModels` exercises exactly the contentKey identity behaviour rc01 changed. Co-Authored-By: Claude Opus 5 (1M context) --- Nav3Navigation/README.md | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Nav3Navigation/README.md b/Nav3Navigation/README.md index 12bfe1d..dee8bac 100644 --- a/Nav3Navigation/README.md +++ b/Nav3Navigation/README.md @@ -45,7 +45,7 @@ implementation("uk.co.appoly.droid:nav3navigation") **Requirements** - `minSdk` **23** (androidx.navigation3 requirement) -- Depends on `androidx.navigation3` **1.2.0-beta01** (alpha result bus is optional; see [Results](#results)) +- Depends on `androidx.navigation3` **1.2.0-rc01** (alpha result bus is optional; see [Results](#results)) - **Predictive back needs the manifest opt-in below API 36.** It defaults to `true` on API 36+, but on API 33–35 the host app must set it explicitly, or pops commit with no gesture animation: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3696d3c..915ffd5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ sandwichVersion = "2.4.0" kotlinxSerialization = "1.11.0" paging = "3.5.1" roomVersion = "2.8.4" -nav3 = "1.2.0-beta01" +nav3 = "1.2.0-rc01" workManager = "2.11.2" kover = "0.9.9" robolectric = "4.16.1" From d1164e6afceff0bd66e8225d5a052e2fee7ec658 Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Thu, 10 Sep 2026 12:20:08 +0100 Subject: [PATCH 09/15] build: Compose BOM 2026.08.00 -> 2026.09.00 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Compose patch release, not a feature one. Diffing the two BOM POMs artifact-by-artifact (248 entries each), every version that moves goes 1.12.0 -> 1.12.1 across animation, foundation, material, runtime and ui. Nothing else changes, and material3 does not move at all. No API changes to absorb, so this is version-only across the ten modules that apply the platform. The fixes do land where this repo lives, though — runtime-saveable and foundation back ComposeExtensions' serialization-safe MutableState holders, SegmentedControl's drag gestures, and the lazy-list paging extensions. Verified with the full test task across all modules, koverVerify, and :app:verifyConsumerKeepRules. Also re-ran Nav3Navigation's on-device suite: animation and foundation both moved, and predictive-back scrubbing plus rememberSaveable restore across recreation are exactly what they drive. 14/14. Co-Authored-By: Claude Opus 5 (1M context) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 915ffd5..103cef5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ androidxTestCore = "1.7.0" lifecycleRuntime = "2.11.0" appcompat = "1.8.0" activityCompose = "1.13.0" -composeBom = "2026.08.00" +composeBom = "2026.09.00" flexiLoggerVersion = "2.1.4" okhttp = "5.5.0" retrofit = "3.0.0" From db9187e9ba37690c0756e36ec7cb164286b6491a Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Thu, 10 Sep 2026 12:20:21 +0100 Subject: [PATCH 10/15] build: Room 2.8.4 -> 2.8.5 A patch bump with no API change, affecting DateHelperUtil-Room's type converters and S3Uploader-Multipart's upload-state database. The thing worth checking was the exported schemas, since S3Uploader-Multipart writes them to a checked-in schemas/ directory and a codegen change there would mean a migration problem rather than a build problem. Room 2.8.5 regenerates them byte-identically: both v1 and v2 keep the same formatVersion and the same identityHash (9fad5f76... and 2b09eace...), so nothing needed re-checking in and no migration is implied. The DateHelperUtil-Room README change is UpdateReadmeVersions syncing the Room coordinates in its install block during the build, not a hand edit. Verified with the full test task across all modules, koverVerify, and :app:verifyConsumerKeepRules. Worth noting the gap this exposed rather than leaving it implicit: S3Uploader-Multipart has no androidTest source set at all, despite androidTestImplementation(room.testing) and schemas wired into androidTest assets, so a @Database(version = 2) with two exported schemas carries no automated migration coverage. Harmless here because the schemas are unchanged, but the next entity change walks into it. Co-Authored-By: Claude Opus 5 (1M context) --- DateHelperUtil-Room/README.md | 6 +++--- gradle/libs.versions.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DateHelperUtil-Room/README.md b/DateHelperUtil-Room/README.md index ad9a688..6d2f9a1 100644 --- a/DateHelperUtil-Room/README.md +++ b/DateHelperUtil-Room/README.md @@ -20,9 +20,9 @@ implementation("uk.co.appoly.droid:datehelperutil:1.9.0") implementation("uk.co.appoly.droid:datehelperutil-room:1.9.0") // Required Room dependencies -implementation("androidx.room:room-runtime:2.8.4") -implementation("androidx.room:room-ktx:2.8.4") -ksp("androidx.room:room-compiler:2.8.4") +implementation("androidx.room:room-runtime:2.8.5") +implementation("androidx.room:room-ktx:2.8.5") +ksp("androidx.room:room-compiler:2.8.5") ``` ## Usage diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 103cef5..745d1b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ retrofit = "3.0.0" sandwichVersion = "2.4.0" kotlinxSerialization = "1.11.0" paging = "3.5.1" -roomVersion = "2.8.4" +roomVersion = "2.8.5" nav3 = "1.2.0-rc01" workManager = "2.11.2" kover = "0.9.9" From faa03256ceb27d430b2f37c9c78a06b159bd7eee Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Fri, 11 Sep 2026 16:26:56 +0100 Subject: [PATCH 11/15] build: group the version catalog by publication blast radius Split [versions], [libraries] and [plugins] into three tiers so it is obvious which bumps consumers can see: - PUBLISHED: on a consumer's classpath (api/implementation in a library module, or baked into the AAR by a code generator). Annotated the `api`-exposed ones (FlexiLogger, sandwich, Navigation 3) since a bump there is a breaking-change candidate rather than a routine build tweak. - BUILD/TEST: test and androidTest configurations of the library modules, plus the toolchain. Only CI can break. - DEMO APP: referenced solely by :app. No entries added, removed or re-versioned in the regroup - activityCompose moves to BUILD/TEST, where it belongs: it is the demo app plus one androidTest dependency in Nav3Navigation, not a published dependency. Also drop the `kover` version and plugin alias. A settings plugins block is resolved before the version catalog exists, so settings.gradle.kts could never have read libs.plugins.kover - it hardcodes the version and always has. With no Renovate or Dependabot on this repo, the entry was two lines that could silently disagree with the real version. settings.gradle.kts is now the single source of truth and its comment says why. Remove the commented-out testImplementation(libs.paging.common) from the two Lazy*PagingExtensions modules; the live usages in :app and BaseRepo-Paging are unaffected. Co-Authored-By: Claude Opus 5 (1M context) --- LazyGridPagingExtensions/build.gradle.kts | 1 - LazyListPagingExtensions/build.gradle.kts | 1 - gradle/libs.versions.toml | 141 ++++++++++++++-------- settings.gradle.kts | 4 +- 4 files changed, 92 insertions(+), 55 deletions(-) diff --git a/LazyGridPagingExtensions/build.gradle.kts b/LazyGridPagingExtensions/build.gradle.kts index 09f9c4e..6ac351d 100644 --- a/LazyGridPagingExtensions/build.gradle.kts +++ b/LazyGridPagingExtensions/build.gradle.kts @@ -57,7 +57,6 @@ dependencies { //Paging implementation(libs.paging.runtime) implementation(libs.paging.compose) -// testImplementation(libs.paging.common) testImplementation(libs.junit) testImplementation(libs.robolectric) diff --git a/LazyListPagingExtensions/build.gradle.kts b/LazyListPagingExtensions/build.gradle.kts index 5d6620d..df65e3c 100644 --- a/LazyListPagingExtensions/build.gradle.kts +++ b/LazyListPagingExtensions/build.gradle.kts @@ -57,7 +57,6 @@ dependencies { //Paging implementation(libs.paging.runtime) implementation(libs.paging.compose) -// testImplementation(libs.paging.common) testImplementation(libs.junit) testImplementation(libs.robolectric) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 745d1b4..ae856c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,56 +1,73 @@ +# ============================================================================= +# Version catalog +# +# Entries are grouped by blast radius: +# +# [PUBLISHED] Reaches consumers of the library. These end up on a consumer's +# classpath (api/implementation => POM runtime scope) or are baked +# into the published AAR by a code generator. Bumping one of these +# is a consumer-visible change - mind binary compatibility, the +# minimum versions consumers must tolerate, and the README tables. +# [BUILD/TEST] Compiles and verifies the library modules but never leaves the +# build - test and androidTest configurations, compilers, tooling. +# Bump freely; only CI can be broken by it. +# [DEMO APP] Used solely by the `app` demo module. Invisible to consumers. +# ============================================================================= + [versions] -agp = "9.4.0" -kotlin = "2.4.20" -vanniktechPublish = "0.37.0" -ksp = "2.3.11" +# --- PUBLISHED: these versions reach consumers ------------------------------- +kotlin = "2.4.20" # also the language/stdlib version consumers compile against coreKtx = "1.19.0" -junit = "4.13.2" -junitVersion = "1.3.0" -espressoCore = "3.7.0" -androidxTestCore = "1.7.0" -lifecycleRuntime = "2.11.0" appcompat = "1.8.0" -activityCompose = "1.13.0" +lifecycleRuntime = "2.11.0" composeBom = "2026.09.00" -flexiLoggerVersion = "2.1.4" +coroutines = "1.11.0" # -android ships; -test is build-only +flexiLoggerVersion = "2.1.4" # exposed as `api` - a bump is a consumer-visible change okhttp = "5.5.0" retrofit = "3.0.0" -sandwichVersion = "2.4.0" +sandwichVersion = "2.4.0" # exposed as `api` from BaseRepo kotlinxSerialization = "1.11.0" paging = "3.5.1" roomVersion = "2.8.5" -nav3 = "1.2.0-rc01" +nav3 = "1.2.0-rc01" # exposed as `api` from Nav3Navigation workManager = "2.11.2" -kover = "0.9.9" + +# --- BUILD/TEST: toolchain + test-only, never published ---------------------- +agp = "9.4.0" +ksp = "2.3.11" +vanniktechPublish = "0.37.0" # release tooling only +junit = "4.13.2" +junitVersion = "1.3.0" +espressoCore = "3.7.0" +androidxTestCore = "1.7.0" robolectric = "4.16.1" -coroutines = "1.11.0" +activityCompose = "1.13.0" # demo app + Nav3Navigation androidTest only [libraries] +# ============================================================================= +# PUBLISHED - on consumers' classpaths +# ============================================================================= + +#AndroidX core androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-test-core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "androidxTestCore" } -robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } -kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } -kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } -androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime", version.ref = "lifecycleRuntime" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime", version.ref = "lifecycleRuntime" } -#Compose -androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +#Kotlin / coroutines +kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } + +#kotlinx serialization +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } +kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinxSerialization" } + +#Compose (BOM is imported by the Compose-facing modules and by the demo app) androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } -androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } -androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } -compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } -#FlexiLogger (Maven Central) +#FlexiLogger (Maven Central) - exposed as `api` flexiLogger = { group = "io.github.projectdelta6", name = "flexilogger", version.ref = "flexiLoggerVersion" } flexiLogger-okhttp = { group = "io.github.projectdelta6", name = "flexilogger-okhttp", version.ref = "flexiLoggerVersion" } @@ -58,37 +75,26 @@ flexiLogger-okhttp = { group = "io.github.projectdelta6", name = "flexilogger-ok okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } okhttp-urlconnection = { module = "com.squareup.okhttp3:okhttp-urlconnection", version.ref = "okhttp" } -okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } #Retrofit retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-serializationConverter = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } -#sandwich (versions aligned by the BOM — import platform(libs.sandwich.bom) alongside these) +#sandwich (versions aligned by the BOM - import platform(libs.sandwich.bom) alongside these) sandwich-bom = { group = "com.github.skydoves", name = "sandwich-bom", version.ref = "sandwichVersion" } sandwich = { group = "com.github.skydoves", name = "sandwich" } sandwich-retrofit = { group = "com.github.skydoves", name = "sandwich-retrofit" } -#Kotlin -kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" } - -#kotlinx serialization -kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } -kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinxSerialization" } - #Paging paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" } paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" } -paging-common = { group = "androidx.paging", name = "paging-common", version.ref = "paging" } -paging-testing = { group = "androidx.paging", name = "paging-testing", version.ref = "paging" } -#Room +#Room (the compiler is `ksp`-only, but its output is baked into the published AAR) androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "roomVersion" } -androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomVersion" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomVersion" } -androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "roomVersion" } +androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomVersion" } -#Navigation 3 +#Navigation 3 - exposed as `api` from Nav3Navigation androidx-navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "nav3" } androidx-navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "nav3" } androidx-lifecycle-viewmodel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "lifecycleRuntime" } @@ -97,15 +103,48 @@ androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "l #WorkManager androidx-work-runtime = { group = "androidx.work", name = "work-runtime", version.ref = "workManager" } + +# ============================================================================= +# BUILD/TEST ONLY - test & androidTest configurations of the library modules +# ============================================================================= + +#Unit test +junit = { group = "junit", name = "junit", version.ref = "junit" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } +paging-common = { group = "androidx.paging", name = "paging-common", version.ref = "paging" } +paging-testing = { group = "androidx.paging", name = "paging-testing", version.ref = "paging" } androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "workManager" } +#Instrumented test +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-test-core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "androidxTestCore" } +androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "roomVersion" } + +#Compose test (versions from the Compose BOM) +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } + +# ============================================================================= +# DEMO APP ONLY - never referenced by a published module +# ============================================================================= +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } + [plugins] -android-application = { id = "com.android.application", version.ref = "agp" } +# --- Applied by the published library modules -------------------------------- android-library = { id = "com.android.library", version.ref = "agp" } -kotlinKSP = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -vanniktech-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechPublish" } +kotlinKSP = { id = "com.google.devtools.ksp", version.ref = "ksp" } room = { id = "androidx.room", version.ref = "roomVersion" } -kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } +vanniktech-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechPublish" } + +# --- Local only -------------------------------------------------------------- +android-application = { id = "com.android.application", version.ref = "agp" } # demo app diff --git a/settings.gradle.kts b/settings.gradle.kts index a35a2f1..8652585 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,8 +13,8 @@ pluginManagement { } // Kover coverage aggregation across the whole build (Android + pure-JVM modules) into a -// single root report. Version kept in sync with `kover` in gradle/libs.versions.toml (the -// settings plugins block can't read the version catalog). +// single root report. The version lives here and nowhere else: a settings plugins block is +// resolved before the version catalog exists, so it can't read gradle/libs.versions.toml. plugins { id("org.jetbrains.kotlinx.kover.aggregation") version "0.9.9" } From 4f53a3db42367b1e5c81c974f9baa3575cd79b88 Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Tue, 15 Sep 2026 09:29:08 +0100 Subject: [PATCH 12/15] build: TOOLBOX_VERSION 1.9.0 -> 1.9.1-rc01 README version references are synced by the UpdateReadmeVersions task during Gradle sync, so they move with the bump rather than being hand-edited. Co-Authored-By: Claude Opus 5 (1M context) --- AppSnackBar-UiState/README.md | 6 +++--- AppSnackBar/README.md | 2 +- BaseRepo-AppolyJson/README.md | 4 ++-- BaseRepo-Paging-AppolyJson/README.md | 10 +++++----- BaseRepo-Paging/README.md | 8 ++++---- BaseRepo-S3Uploader-Multipart/README.md | 6 +++--- BaseRepo-S3Uploader/README.md | 6 +++--- BaseRepo/README.md | 2 +- ComposeExtensions/README.md | 2 +- ConnectivityMonitor/README.md | 2 +- DateHelperUtil-Room/README.md | 4 ++-- DateHelperUtil-Serialization/README.md | 4 ++-- DateHelperUtil/README.md | 2 +- LazyGridPagingExtensions/README.md | 4 ++-- LazyListPagingExtensions/README.md | 4 ++-- MockInterceptor-AppolyJson/README.md | 2 +- MockInterceptor-Retrofit/README.md | 2 +- MockInterceptor-Serialization/README.md | 2 +- MockInterceptor/README.md | 2 +- Nav3Navigation/README.md | 4 ++-- PagingExtensions/README.md | 2 +- README.md | 8 ++++---- S3Uploader-Multipart/README.md | 2 +- S3Uploader/README.md | 2 +- SegmentedControl/README.md | 2 +- UiState/README.md | 2 +- buildSrc/src/main/kotlin/BuildConfig.kt | 2 +- 27 files changed, 49 insertions(+), 49 deletions(-) diff --git a/AppSnackBar-UiState/README.md b/AppSnackBar-UiState/README.md index 5c7c84b..3e66462 100644 --- a/AppSnackBar-UiState/README.md +++ b/AppSnackBar-UiState/README.md @@ -13,9 +13,9 @@ Integration module that bridges the AppSnackBar and UiState modules, providing a ```gradle.kts // Requires both base modules -implementation("uk.co.appoly.droid:uistate:1.9.0") -implementation("uk.co.appoly.droid:appsnackbar:1.9.0") -implementation("uk.co.appoly.droid:appsnackbar-uistate:1.9.0") +implementation("uk.co.appoly.droid:uistate:1.9.1-rc01") +implementation("uk.co.appoly.droid:appsnackbar:1.9.1-rc01") +implementation("uk.co.appoly.droid:appsnackbar-uistate:1.9.1-rc01") ``` ## Usage diff --git a/AppSnackBar/README.md b/AppSnackBar/README.md index 929aa99..7508ca4 100644 --- a/AppSnackBar/README.md +++ b/AppSnackBar/README.md @@ -13,7 +13,7 @@ A customizable Jetpack Compose Snackbar implementation with support for differen ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:appsnackbar:1.9.0") +implementation("uk.co.appoly.droid:appsnackbar:1.9.1-rc01") ``` ## Usage diff --git a/BaseRepo-AppolyJson/README.md b/BaseRepo-AppolyJson/README.md index ebacda9..1c8e78d 100644 --- a/BaseRepo-AppolyJson/README.md +++ b/BaseRepo-AppolyJson/README.md @@ -14,8 +14,8 @@ Appoly's JSON format. ```gradle.kts // Requires the base BaseRepo module -implementation("uk.co.appoly.droid:baserepo:1.9.0") -implementation("uk.co.appoly.droid:baserepo-appolyjson:1.9.0") +implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo-appolyjson:1.9.1-rc01") ``` ## API Response Structure diff --git a/BaseRepo-Paging-AppolyJson/README.md b/BaseRepo-Paging-AppolyJson/README.md index 2b2f8a7..bf8e873 100644 --- a/BaseRepo-Paging-AppolyJson/README.md +++ b/BaseRepo-Paging-AppolyJson/README.md @@ -15,13 +15,13 @@ follow Appoly's paging format. ```gradle.kts // Requires the base modules -implementation("uk.co.appoly.droid:baserepo:1.9.0") -implementation("uk.co.appoly.droid:baserepo-paging:1.9.0") -implementation("uk.co.appoly.droid:baserepo-paging-appolyjson:1.9.0") +implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo-paging:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo-paging-appolyjson:1.9.1-rc01") // For Compose UI integration -implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.0") // For LazyColumn -implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.0") // For LazyGrid +implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.1-rc01") // For LazyColumn +implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.1-rc01") // For LazyGrid ``` ## API Response Format diff --git a/BaseRepo-Paging/README.md b/BaseRepo-Paging/README.md index b8e37e4..9bacbe0 100644 --- a/BaseRepo-Paging/README.md +++ b/BaseRepo-Paging/README.md @@ -17,12 +17,12 @@ extended for specific JSON formats. ```gradle.kts // Requires the base BaseRepo module -implementation("uk.co.appoly.droid:baserepo:1.9.0") -implementation("uk.co.appoly.droid:baserepo-paging:1.9.0") +implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo-paging:1.9.1-rc01") // For Compose UI integration -implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.0") // For LazyColumn -implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.0") // For LazyGrid +implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.1-rc01") // For LazyColumn +implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.1-rc01") // For LazyGrid ``` ## Extensions diff --git a/BaseRepo-S3Uploader-Multipart/README.md b/BaseRepo-S3Uploader-Multipart/README.md index 0227437..c197fdc 100644 --- a/BaseRepo-S3Uploader-Multipart/README.md +++ b/BaseRepo-S3Uploader-Multipart/README.md @@ -15,9 +15,9 @@ Extension module that bridges BaseRepo and S3Uploader-Multipart, enabling pausab ```gradle.kts // Requires the base modules -implementation("uk.co.appoly.droid:baserepo:1.9.0") -implementation("uk.co.appoly.droid:s3uploader-multipart:1.9.0") -implementation("uk.co.appoly.droid:baserepo-s3uploader-multipart:1.9.0") +implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") +implementation("uk.co.appoly.droid:s3uploader-multipart:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo-s3uploader-multipart:1.9.1-rc01") ``` ## Usage diff --git a/BaseRepo-S3Uploader/README.md b/BaseRepo-S3Uploader/README.md index cad0a2d..2a00fe2 100644 --- a/BaseRepo-S3Uploader/README.md +++ b/BaseRepo-S3Uploader/README.md @@ -18,9 +18,9 @@ An extension module that bridges BaseRepo and S3Uploader, enabling seamless file ```gradle.kts // Requires both the base modules -implementation("uk.co.appoly.droid:baserepo:1.9.0") -implementation("uk.co.appoly.droid:s3uploader:1.9.0") -implementation("uk.co.appoly.droid:baserepo-s3uploader:1.9.0") +implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") +implementation("uk.co.appoly.droid:s3uploader:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo-s3uploader:1.9.1-rc01") ``` ## How it Works diff --git a/BaseRepo/README.md b/BaseRepo/README.md index 8719440..9b9ffc3 100644 --- a/BaseRepo/README.md +++ b/BaseRepo/README.md @@ -14,7 +14,7 @@ Foundation module for implementing the repository pattern with standardized API ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:baserepo:1.9.0") +implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") ``` ## Extensions diff --git a/ComposeExtensions/README.md b/ComposeExtensions/README.md index c3b70af..6387b4c 100644 --- a/ComposeExtensions/README.md +++ b/ComposeExtensions/README.md @@ -13,7 +13,7 @@ Compose utilities for insets/IME padding, padding arithmetic, serialization-safe ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:composeextensions:1.9.0") +implementation("uk.co.appoly.droid:composeextensions:1.9.1-rc01") ``` ## Usage diff --git a/ConnectivityMonitor/README.md b/ConnectivityMonitor/README.md index f101039..0992c68 100644 --- a/ConnectivityMonitor/README.md +++ b/ConnectivityMonitor/README.md @@ -9,7 +9,7 @@ Add the following dependency to your project's `build.gradle` file: ```gradle.kts -implementation("uk.co.appoly.droid:connectivitymonitor:1.9.0") +implementation("uk.co.appoly.droid:connectivitymonitor:1.9.1-rc01") ``` ## Usage diff --git a/DateHelperUtil-Room/README.md b/DateHelperUtil-Room/README.md index 6d2f9a1..364e7c0 100644 --- a/DateHelperUtil-Room/README.md +++ b/DateHelperUtil-Room/README.md @@ -16,8 +16,8 @@ Extension module for DateHelperUtil that provides Room database integration for ```gradle.kts // Requires base DateHelperUtil module -implementation("uk.co.appoly.droid:datehelperutil:1.9.0") -implementation("uk.co.appoly.droid:datehelperutil-room:1.9.0") +implementation("uk.co.appoly.droid:datehelperutil:1.9.1-rc01") +implementation("uk.co.appoly.droid:datehelperutil-room:1.9.1-rc01") // Required Room dependencies implementation("androidx.room:room-runtime:2.8.5") diff --git a/DateHelperUtil-Serialization/README.md b/DateHelperUtil-Serialization/README.md index 0a5de23..f02ede8 100644 --- a/DateHelperUtil-Serialization/README.md +++ b/DateHelperUtil-Serialization/README.md @@ -16,8 +16,8 @@ Extension module for DateHelperUtil that provides kotlinx.serialization integrat ```gradle.kts // Requires base DateHelperUtil module -implementation("uk.co.appoly.droid:datehelperutil:1.9.0") -implementation("uk.co.appoly.droid:datehelperutil-serialization:1.9.0") +implementation("uk.co.appoly.droid:datehelperutil:1.9.1-rc01") +implementation("uk.co.appoly.droid:datehelperutil-serialization:1.9.1-rc01") // Required kotlinx.serialization dependencies implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0") diff --git a/DateHelperUtil/README.md b/DateHelperUtil/README.md index fce722a..6128c51 100644 --- a/DateHelperUtil/README.md +++ b/DateHelperUtil/README.md @@ -14,7 +14,7 @@ A utility module for standardized date and time operations in Android applicatio ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:datehelperutil:1.9.0") +implementation("uk.co.appoly.droid:datehelperutil:1.9.1-rc01") ``` ## 1.4.1 patch note diff --git a/LazyGridPagingExtensions/README.md b/LazyGridPagingExtensions/README.md index 3b7156d..5f9b564 100644 --- a/LazyGridPagingExtensions/README.md +++ b/LazyGridPagingExtensions/README.md @@ -15,8 +15,8 @@ Extension functions for integrating Jetpack Paging 3 with Compose LazyVerticalGr ```gradle.kts // Requires the base PagingExtensions module -implementation("uk.co.appoly.droid:pagingextensions:1.9.0") -implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.0") +implementation("uk.co.appoly.droid:pagingextensions:1.9.1-rc01") +implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.1-rc01") // Make sure to include Jetpack Paging Compose implementation("androidx.paging:paging-compose:3.5.1") diff --git a/LazyListPagingExtensions/README.md b/LazyListPagingExtensions/README.md index 26076ff..9cffee0 100644 --- a/LazyListPagingExtensions/README.md +++ b/LazyListPagingExtensions/README.md @@ -15,8 +15,8 @@ Extension functions for easy integration of Jetpack Paging 3 with Compose LazyCo ```gradle.kts // Requires the base PagingExtensions module -implementation("uk.co.appoly.droid:pagingextensions:1.9.0") -implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.0") +implementation("uk.co.appoly.droid:pagingextensions:1.9.1-rc01") +implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.1-rc01") // Make sure to include Jetpack Paging Compose implementation("androidx.paging:paging-compose:3.5.1") diff --git a/MockInterceptor-AppolyJson/README.md b/MockInterceptor-AppolyJson/README.md index e85ada7..74149b5 100644 --- a/MockInterceptor-AppolyJson/README.md +++ b/MockInterceptor-AppolyJson/README.md @@ -14,7 +14,7 @@ Extension for [MockInterceptor-Serialization](../MockInterceptor-Serialization/) ```gradle.kts // MockInterceptor and MockInterceptor-Serialization are included transitively -implementation("uk.co.appoly.droid:mockinterceptor-appolyjson:1.9.0") +implementation("uk.co.appoly.droid:mockinterceptor-appolyjson:1.9.1-rc01") ``` ## Usage diff --git a/MockInterceptor-Retrofit/README.md b/MockInterceptor-Retrofit/README.md index 5572e72..6e0949e 100644 --- a/MockInterceptor-Retrofit/README.md +++ b/MockInterceptor-Retrofit/README.md @@ -13,7 +13,7 @@ Extension for [MockInterceptor](../MockInterceptor/) that reads Retrofit HTTP an ```gradle.kts // MockInterceptor is included transitively -implementation("uk.co.appoly.droid:mockinterceptor-retrofit:1.9.0") +implementation("uk.co.appoly.droid:mockinterceptor-retrofit:1.9.1-rc01") ``` > **Note:** Retrofit is a `compileOnly` dependency — your project must already depend on Retrofit. diff --git a/MockInterceptor-Serialization/README.md b/MockInterceptor-Serialization/README.md index 59c2bb9..c970e7e 100644 --- a/MockInterceptor-Serialization/README.md +++ b/MockInterceptor-Serialization/README.md @@ -12,7 +12,7 @@ Extension for [MockInterceptor](../MockInterceptor/) that adds type-safe JSON re ```gradle.kts // MockInterceptor is included transitively -implementation("uk.co.appoly.droid:mockinterceptor-serialization:1.9.0") +implementation("uk.co.appoly.droid:mockinterceptor-serialization:1.9.1-rc01") ``` ## Usage diff --git a/MockInterceptor/README.md b/MockInterceptor/README.md index d37dbae..9642f7b 100644 --- a/MockInterceptor/README.md +++ b/MockInterceptor/README.md @@ -16,7 +16,7 @@ An OkHttp interceptor with a route-matching DSL for mocking API responses during ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:mockinterceptor:1.9.0") +implementation("uk.co.appoly.droid:mockinterceptor:1.9.1-rc01") ``` ## Usage diff --git a/Nav3Navigation/README.md b/Nav3Navigation/README.md index dee8bac..9c685d0 100644 --- a/Nav3Navigation/README.md +++ b/Nav3Navigation/README.md @@ -32,13 +32,13 @@ without giving up the fused-screen / ambient-navigator convenience that Voyager ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:nav3navigation:1.9.0") +implementation("uk.co.appoly.droid:nav3navigation:1.9.1-rc01") ``` Or via the AppolyDroid BOM (version managed by the platform): ```gradle.kts -implementation(platform("uk.co.appoly.droid:bom:1.9.0")) +implementation(platform("uk.co.appoly.droid:bom:1.9.1-rc01")) implementation("uk.co.appoly.droid:nav3navigation") ``` diff --git a/PagingExtensions/README.md b/PagingExtensions/README.md index 4c8f405..7686a30 100644 --- a/PagingExtensions/README.md +++ b/PagingExtensions/README.md @@ -12,7 +12,7 @@ Core utilities and extensions for Jetpack Paging 3 integration, providing the fo ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:pagingextensions:1.9.0") +implementation("uk.co.appoly.droid:pagingextensions:1.9.1-rc01") ``` ## Usage diff --git a/README.md b/README.md index bfcd3cb..1b88fbc 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ In your `libs.versions.toml` file: ```toml [versions] -appolydroidToolbox = "1.9.0" # Replace with the latest version +appolydroidToolbox = "1.9.1-rc01" # Replace with the latest version [libraries] appolydroid-toolbox-bom = { group = "uk.co.appoly.droid", name = "bom", version.ref = "appolydroidToolbox" } @@ -129,7 +129,7 @@ In your module's `build.gradle.kts`: ```gradle.kts dependencies { // Import the BOM - implementation(platform("uk.co.appoly.droid:bom:1.9.0")) + implementation(platform("uk.co.appoly.droid:bom:1.9.1-rc01")) // Now you can use AppolyDroid modules without specifying versions implementation("uk.co.appoly.droid:baserepo") @@ -166,7 +166,7 @@ In your `libs.versions.toml` file: ```toml [versions] -appolydroidToolbox = "1.9.0" # Replace with the latest version +appolydroidToolbox = "1.9.1-rc01" # Replace with the latest version [libraries] #AppolyDroid-Toolbox @@ -234,7 +234,7 @@ In your module's `build.gradle.kts`: ```gradle.kts dependencies { - val appolydroidToolbox = "1.9.0" // Replace with the latest version + val appolydroidToolbox = "1.9.1-rc01" // Replace with the latest version // Add only the modules you need implementation("uk.co.appoly.droid:baserepo:$appolydroidToolbox") implementation("uk.co.appoly.droid:baserepo-appolyjson:$appolydroidToolbox") diff --git a/S3Uploader-Multipart/README.md b/S3Uploader-Multipart/README.md index 59d62c7..db3835e 100644 --- a/S3Uploader-Multipart/README.md +++ b/S3Uploader-Multipart/README.md @@ -16,7 +16,7 @@ Advanced S3 upload module with pause, resume, and recovery support using AWS S3 ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:s3uploader-multipart:1.9.0") +implementation("uk.co.appoly.droid:s3uploader-multipart:1.9.1-rc01") ``` This module depends on `S3Uploader` and includes it transitively. diff --git a/S3Uploader/README.md b/S3Uploader/README.md index ef00776..4918598 100644 --- a/S3Uploader/README.md +++ b/S3Uploader/README.md @@ -16,7 +16,7 @@ Standalone module for Amazon S3 file uploading with progress tracking and error ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:s3uploader:1.9.0") +implementation("uk.co.appoly.droid:s3uploader:1.9.1-rc01") ``` ## Usage diff --git a/SegmentedControl/README.md b/SegmentedControl/README.md index f81df40..255ebd6 100644 --- a/SegmentedControl/README.md +++ b/SegmentedControl/README.md @@ -17,7 +17,7 @@ A highly customizable iOS-style segmented control for Jetpack Compose with smoot ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:segmentedcontrol:1.9.0") +implementation("uk.co.appoly.droid:segmentedcontrol:1.9.1-rc01") ``` ## Usage diff --git a/UiState/README.md b/UiState/README.md index 77a5319..6649af5 100644 --- a/UiState/README.md +++ b/UiState/README.md @@ -13,7 +13,7 @@ A standardized UI state management library for Android applications, providing c ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:uistate:1.9.0") +implementation("uk.co.appoly.droid:uistate:1.9.1-rc01") ``` ## Usage diff --git a/buildSrc/src/main/kotlin/BuildConfig.kt b/buildSrc/src/main/kotlin/BuildConfig.kt index fc7e0c9..2bc7b19 100644 --- a/buildSrc/src/main/kotlin/BuildConfig.kt +++ b/buildSrc/src/main/kotlin/BuildConfig.kt @@ -9,7 +9,7 @@ object BuildConfig { * The current version of the AppolyDroid Toolbox library. * This is used for maven publishing and README version updates. */ - const val TOOLBOX_VERSION = "1.9.0" + const val TOOLBOX_VERSION = "1.9.1-rc01" /** * SDK version configuration for Android modules. From 51c3ddbf7a282ee1bdf3681b0c61afecc2e9979a Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Tue, 15 Sep 2026 09:29:18 +0100 Subject: [PATCH 13/15] build: add local-publish and clear-local-publish scripts and run configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Testing a change before a release had one path — publish.sh --local — which signs, so it waits on a 1Password unlock for a signature nothing local ever verifies: Gradle does not check signatures on resolve. That makes the everyday iteration loop cost a vault unlock for no benefit, and there was no way to undo an install short of deleting from ~/.m2 by hand. publish-local.sh installs unsigned and needs no credentials, and takes module names to publish a subset while iterating on one module. --signed delegates to publish.sh --local for the cases where the artifact set itself is under test. clear-local-publish.sh removes the install again, optionally for one version. It lists what it will delete and confirms first, and only ever touches the toolbox's own group directory — derived from PUBLISH_GROUP, so a fork clears its own coordinates rather than ours. Both are also shared Android Studio run configurations under .run/, running in the Run window's terminal so the confirmation prompt works there. CONTRIBUTING.md gains the consuming-project half, which was missing entirely: where mavenLocal() goes, why the first resolve needs --refresh-dependencies in both directions, and the escape from version shadowing — a TOOLBOX_VERSION that cannot exist on Central, so no version string means two different things. Co-Authored-By: Claude Opus 5 (1M context) --- .run/Clear_Local_Maven_Publish.run.xml | 17 +++ .run/Publish_to_Maven_Local.run.xml | 17 +++ .run/Publish_to_Maven_Local__signed_.run.xml | 17 +++ CLAUDE.md | 3 + CONTRIBUTING.md | 107 +++++++++++++-- scripts/clear-local-publish.sh | 133 +++++++++++++++++++ scripts/publish-local.sh | 112 ++++++++++++++++ scripts/publish.sh | 6 + 8 files changed, 400 insertions(+), 12 deletions(-) create mode 100644 .run/Clear_Local_Maven_Publish.run.xml create mode 100644 .run/Publish_to_Maven_Local.run.xml create mode 100644 .run/Publish_to_Maven_Local__signed_.run.xml create mode 100755 scripts/clear-local-publish.sh create mode 100755 scripts/publish-local.sh diff --git a/.run/Clear_Local_Maven_Publish.run.xml b/.run/Clear_Local_Maven_Publish.run.xml new file mode 100644 index 0000000..71deee6 --- /dev/null +++ b/.run/Clear_Local_Maven_Publish.run.xml @@ -0,0 +1,17 @@ + + + + diff --git a/.run/Publish_to_Maven_Local.run.xml b/.run/Publish_to_Maven_Local.run.xml new file mode 100644 index 0000000..e4833ba --- /dev/null +++ b/.run/Publish_to_Maven_Local.run.xml @@ -0,0 +1,17 @@ + + + + diff --git a/.run/Publish_to_Maven_Local__signed_.run.xml b/.run/Publish_to_Maven_Local__signed_.run.xml new file mode 100644 index 0000000..17658e1 --- /dev/null +++ b/.run/Publish_to_Maven_Local__signed_.run.xml @@ -0,0 +1,17 @@ + + + + diff --git a/CLAUDE.md b/CLAUDE.md index 3c44c5e..e02b0d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -125,6 +125,9 @@ Published to **Maven Central** under `uk.co.appoly.droid`, with lowercase artifa Gradle reads them only under the `ORG_GRADLE_PROJECT_` prefix with exact camelCase. The vault item is set in the git-ignored `scripts/publish.conf` — this repo is public, so it is not committed. See `scripts/publish.conf.example`. +- `./scripts/publish-local.sh` installs to `~/.m2` unsigned with no credentials (the everyday + local-testing loop), and `./scripts/clear-local-publish.sh` removes that install again. Both are + also Android Studio run configurations in `.run/`. - `./scripts/publish.sh --local` publishes signed artifacts to `~/.m2`; without `--local` it releases to Central. **Releases are run manually and locally** — there is no release CI job and no Maven Central secrets in the repo, so a version tag publishes nothing on its own. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3db6fe2..bad6619 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,23 +6,105 @@ covers building, testing and releasing it. ## Testing an unreleased change Maven Central publishes only what is released, so there is no equivalent of JitPack's -build-any-branch behaviour. Two options replace it. +build-any-branch behaviour — and releases are immutable, so a mistake cannot be corrected in +place. Test locally first. Two options replace JitPack. -**Install locally.** From a checkout of the branch you want to test: +### Install locally (the normal loop) + +From a checkout of the branch you want to test: ```bash -./scripts/publish.sh --local +./scripts/publish-local.sh # every module, unsigned — no credentials needed +./scripts/publish-local.sh BaseRepo UiState # only those modules, for a tight iteration loop +./scripts/publish-local.sh --signed # every module, signed (= ./scripts/publish.sh --local) +``` + +Undo it with: + +```bash +./scripts/clear-local-publish.sh # every locally installed version +./scripts/clear-local-publish.sh 1.9.1-local1 # just that version +./scripts/clear-local-publish.sh --dry-run # list what would go, delete nothing +``` + +Both are also Android Studio run configurations, checked in under `.run/` and shared through +version control: **Publish to Maven Local**, **Publish to Maven Local (signed)** and **Clear Local +Maven Publish**. They run in the Run window's terminal, so the clear script's confirmation prompt +works there. To publish a subset from the IDE, edit the run configuration's *Script options* field — +or just use the terminal. + +`clear-local-publish.sh` only ever touches `~/.m2/repository/uk/co/appoly/droid` (or `PUBLISH_GROUP` +from `scripts/publish.conf`, for a fork). Nothing else in `~/.m2` is read or written. + +**Signed or not?** Unsigned is the default because signing needs the release key out of 1Password, +and Gradle does not verify signatures on resolve — an unsigned local install behaves identically to +a signed one for every purpose this loop has. Use `--signed` only when the thing under test *is* the +signing, or the exact artifact set a release would upload. That path is `publish.sh --local`, which +`--signed` simply delegates to. + +### Consuming a local install from another project + +Add `mavenLocal()` **first** in the consuming project's repository list, so it wins over Central: + +```kotlin +// settings.gradle.kts +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenLocal() + google() + mavenCentral() + } +} ``` -That publishes every module to `~/.m2`, signed. Add `mavenLocal()` to the consuming project's -repositories, ahead of `mavenCentral()`. +Then depend on the toolbox exactly as usual — the BOM works unchanged, since it is installed +locally alongside everything else: + +```kotlin +// is whatever TOOLBOX_VERSION you just installed — this file is not version-synced, +// so read it out of buildSrc/src/main/kotlin/BuildConfig.kt rather than trusting a number here. +implementation(platform("uk.co.appoly.droid:bom:")) +implementation("uk.co.appoly.droid:baserepo") +implementation("uk.co.appoly.droid:uistate") +``` + +Sync with `--refresh-dependencies` the first time: + +```bash +./gradlew --refresh-dependencies :app:assembleStagingDebug +``` + +Without it Gradle may serve a cached module for that version string — resolved earlier from Central +— and never look in `~/.m2` at all. The same applies in reverse *after* clearing: a consumer that +already resolved the local copy keeps serving it until refreshed. + +A narrower alternative, if you would rather `mavenLocal()` could not possibly shadow anything else, +is to scope it to the toolbox group: + +```kotlin +exclusiveContent { + forRepository { mavenLocal() } + filter { includeGroup("uk.co.appoly.droid") } +} +``` + +> **Take `mavenLocal()` back out when you are done**, and run `clear-local-publish.sh`. A local +> install carries the same version string as the real release, so leaving either in place means +> resolving your own working tree while believing you are testing the published artifacts. A partial +> install (`publish-local.sh BaseRepo`) is worse still: the other modules in `~/.m2` are whatever was +> installed last, possibly a different build of the same version. + +The cleanest way to remove the ambiguity entirely is to bump `TOOLBOX_VERSION` in +`buildSrc/src/main/kotlin/BuildConfig.kt` to something that does not and will not exist on Central — +`1.9.1-local1` — and depend on that from the consuming project. Then there is no version string in +play that could mean two different things, and the dependency-cache problem disappears with it. +Revert the bump before committing. -> Take `mavenLocal()` out again before committing, and before drawing any conclusion about a -> released version. A locally published build carries the same version string as the real one, so -> leaving it in means resolving your own artifacts while believing you are testing the release. +### Publish a snapshot -**Publish a snapshot.** Snapshot versions go to Central's snapshot repository rather than the main -one, and need it adding explicitly: +Snapshot versions go to Central's snapshot repository rather than the main one, and need it adding +explicitly: ```kotlin maven { url = uri("https://central.sonatype.com/repository/maven-snapshots/") } @@ -50,7 +132,8 @@ Bump `TOOLBOX_VERSION` in `buildSrc/src/main/kotlin/BuildConfig.kt` first. Every one version; see [Why one version for all modules](#why-one-version-for-all-modules). > **Releases are immutable.** A version can never be re-uploaded or corrected — the only remedy is -> publishing a new one. Iterate with `--local` *before* releasing, never after. +> publishing a new one. Iterate with [`publish-local.sh`](#install-locally-the-normal-loop) *before* +> releasing, never after. ### Central publishing limits — batch releases, do not split modules @@ -67,7 +150,7 @@ Two consequences for release practice: - **Batch patch releases.** A flurry of same-month point releases — the 1.8.0 → 1.8.3 pattern of August 2026 — would be ~2,540 files, over twice the allowance. Fold fixes into one version and - iterate through `--local` or a snapshot in the meantime. + iterate through a local install or a snapshot in the meantime. - **Do not split modules to reduce usage; it does the opposite.** 26 separately-published repositories would be 26 release events per version, past the limit of 7 on day one. The single batched deployment is the cheapest possible shape under these rules — a further reason for the diff --git a/scripts/clear-local-publish.sh b/scripts/clear-local-publish.sh new file mode 100755 index 0000000..d02c11b --- /dev/null +++ b/scripts/clear-local-publish.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# +# AppolyDroid Toolbox — remove a local install. +# +# Deletes the toolbox from the local Maven repository (~/.m2/repository), undoing +# scripts/publish-local.sh or scripts/publish.sh --local. +# +# ./scripts/clear-local-publish.sh remove every locally installed version +# ./scripts/clear-local-publish.sh 1.9.1-rc01 remove just that version +# ./scripts/clear-local-publish.sh --dry-run list what would go, delete nothing +# ./scripts/clear-local-publish.sh --yes skip the confirmation prompt +# +# WHY THIS MATTERS. A local install carries the same version string as the real release, and +# mavenLocal() wins over mavenCentral(). Left in place it silently shadows the published artifacts: +# the consuming project resolves your working tree while the version number says otherwise. Clearing +# it is how you get back to testing what consumers actually receive. +# +# Only ever touches the toolbox's own group directory — nothing else in ~/.m2 is read or written. +# +set -euo pipefail + +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Shares fork-specific settings (PUBLISH_GROUP) with publish.sh. Git-ignored; see publish.conf.example. +# shellcheck disable=SC1091 +[[ -f scripts/publish.conf ]] && source scripts/publish.conf + +readonly GROUP="${PUBLISH_GROUP:-uk.co.appoly.droid}" +readonly M2_REPO="${M2_REPO:-$HOME/.m2/repository}" +readonly GROUP_DIR="$M2_REPO/${GROUP//.//}" + +RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'; BOLD=$'\033[1m'; NC=$'\033[0m' +info() { echo "${GREEN}[INFO]${NC} $1"; } +warn() { echo "${YELLOW}[WARN]${NC} $1"; } +fail() { echo "${RED}[ERROR]${NC} $1" >&2; } + +DRY_RUN=false +ASSUME_YES=false +VERSION="" +for arg in "$@"; do + case "$arg" in + --dry-run|-n) DRY_RUN=true ;; + --yes|-y) ASSUME_YES=true ;; + -h|--help) + cat <<'USAGE' +Usage: ./scripts/clear-local-publish.sh [--dry-run] [--yes] [VERSION] + + (no args) Remove every locally installed toolbox version from ~/.m2. + VERSION Remove only that version (e.g. 1.9.1-rc01). + --dry-run, -n List what would be removed, delete nothing. + --yes, -y Do not ask for confirmation. + +Only the toolbox's own group directory is touched. Install again with +./scripts/publish-local.sh. +USAGE + exit 0 ;; + -*) fail "Unknown option: $arg"; echo "Try --help" >&2; exit 1 ;; + *) + [[ -z "$VERSION" ]] || { fail "Only one version can be given (got '$VERSION' and '$arg')."; exit 1; } + VERSION="$arg" ;; + esac +done + +if [[ ! -d "$GROUP_DIR" ]]; then + info "Nothing to clear — $GROUP is not installed in $M2_REPO." + exit 0 +fi + +# Collect the artifact/version directories to delete, so the prompt shows exactly what goes. +TARGETS=() +while IFS= read -r dir; do + TARGETS+=("$dir") +done < <( + if [[ -n "$VERSION" ]]; then + find "$GROUP_DIR" -mindepth 2 -maxdepth 2 -type d -name "$VERSION" | sort + else + find "$GROUP_DIR" -mindepth 1 -maxdepth 1 -type d | sort + fi +) + +if [[ ${#TARGETS[@]} -eq 0 ]]; then + if [[ -n "$VERSION" ]]; then + info "Nothing to clear — no module of $GROUP is installed at version $VERSION." + else + info "Nothing to clear — $GROUP_DIR holds no module directories." + fi + exit 0 +fi + +SIZE=$(du -sh "$GROUP_DIR" 2>/dev/null | cut -f1 | tr -d "[:space:]" || echo "?") + +echo +echo "================================================" +if [[ -n "$VERSION" ]]; then + echo " Clearing ${BOLD}$GROUP${NC} ${BOLD}$VERSION${NC} from ~/.m2" +else + echo " Clearing ${BOLD}$GROUP${NC} (all versions) from ~/.m2" +fi +echo "================================================" +echo +echo "${#TARGETS[@]} director$([[ ${#TARGETS[@]} -eq 1 ]] && echo "y" || echo "ies") under $GROUP_DIR:" +for target in "${TARGETS[@]}"; do + echo " ${target#"$GROUP_DIR"/}" +done +echo +info "Group directory currently uses $SIZE on disk." + +if [[ "$DRY_RUN" == true ]]; then + echo + info "Dry run — nothing was deleted." + exit 0 +fi + +if [[ "$ASSUME_YES" != true ]]; then + echo + read -rp "Delete these? (y/N) " -n 1 reply; echo + [[ $reply =~ ^[Yy]$ ]] || { info "Cancelled — nothing was deleted."; exit 0; } +fi + +for target in "${TARGETS[@]}"; do + rm -rf "$target" +done + +# With a version filter the artifact directories survive, holding other versions — or nothing, if +# that was the only one installed. Prune the husks so a later run reports honestly instead of +# listing empty directories. +find "$GROUP_DIR" -mindepth 1 -type d -empty -delete +rmdir "$GROUP_DIR" 2>/dev/null || true + +echo +info "Cleared. The consuming project now resolves $GROUP from its remote repositories again." +warn "Gradle caches resolved modules per project. If a consumer already resolved the local copy," +warn "it needs --refresh-dependencies to notice, or it keeps serving the build you just deleted." diff --git a/scripts/publish-local.sh b/scripts/publish-local.sh new file mode 100755 index 0000000..8b0036b --- /dev/null +++ b/scripts/publish-local.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# +# AppolyDroid Toolbox — local install. +# +# Publishes the toolbox to the local Maven repository (~/.m2/repository) so a consuming app can +# resolve the current working tree through mavenLocal(). This is the loop for testing a change +# BEFORE releasing it — Maven Central releases are immutable, so there is no fixing one afterwards. +# +# ./scripts/publish-local.sh every module, unsigned +# ./scripts/publish-local.sh BaseRepo UiState only those modules (and their dependencies) +# ./scripts/publish-local.sh --signed every module, signed — same as publish.sh --local +# +# WHY UNSIGNED BY DEFAULT. Signing needs the release key out of 1Password, which makes the quick +# iteration loop wait on a vault unlock for a signature nothing local ever verifies. Gradle does not +# check signatures on resolve, so an unsigned local install behaves identically to a signed one for +# every purpose this script exists for. Use --signed when the thing being tested IS the signing or +# the exact artifact set a release would upload; that path is scripts/publish.sh --local. +# +# Undo with ./scripts/clear-local-publish.sh — see CONTRIBUTING.md, "Testing an unreleased change". +# +set -euo pipefail + +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Shares fork-specific settings (PUBLISH_GROUP) with publish.sh. Git-ignored; see publish.conf.example. +# shellcheck disable=SC1091 +[[ -f scripts/publish.conf ]] && source scripts/publish.conf + +readonly GROUP="${PUBLISH_GROUP:-uk.co.appoly.droid}" + +RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'; BOLD=$'\033[1m'; NC=$'\033[0m' +info() { echo "${GREEN}[INFO]${NC} $1"; } +warn() { echo "${YELLOW}[WARN]${NC} $1"; } +fail() { echo "${RED}[ERROR]${NC} $1" >&2; } + +MODULES=() +for arg in "$@"; do + case "$arg" in + --signed|-s) + info "Delegating to scripts/publish.sh --local for the signed install." + exec ./scripts/publish.sh --local + ;; + -h|--help) + cat <<'USAGE' +Usage: ./scripts/publish-local.sh [--signed] [Module ...] + + (no args) Publish every module to ~/.m2, unsigned. Needs no credentials. + Module ... Publish only the named modules, by Gradle project name + (e.g. BaseRepo UiState). Faster when iterating on one module. + --signed, -s Publish every module signed, via scripts/publish.sh --local. + Needs the release signing key. + +Remove a local install again with ./scripts/clear-local-publish.sh. +USAGE + exit 0 ;; + -*) fail "Unknown option: $arg"; echo "Try --help" >&2; exit 1 ;; + *) MODULES+=("${arg#:}") ;; + esac +done + +VERSION=$(sed -n 's/.*TOOLBOX_VERSION *= *"\([^"]*\)".*/\1/p' buildSrc/src/main/kotlin/BuildConfig.kt) +[[ -n "$VERSION" ]] || { fail "Could not read TOOLBOX_VERSION from buildSrc/src/main/kotlin/BuildConfig.kt"; exit 1; } + +# Publishing drives Dokka across every module in one daemon, which needs more metaspace than a +# typical personal ~/.gradle/gradle.properties allows — and user-level properties beat the repo's, +# so the project cannot set this itself. Unpinned, this fails with a bare "Metaspace" error on an +# arbitrary module. Same reasoning as publish.sh. +export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=2048m -Dfile.encoding=UTF-8" + +if [[ ${#MODULES[@]} -eq 0 ]]; then + info "Publishing all modules of ${BOLD}$GROUP${NC} at ${BOLD}$VERSION${NC} to ~/.m2 (unsigned)..." + TASKS=(publishToMavenLocal) +else + # A partial install leaves ~/.m2 holding a mix of versions across modules. That is fine while + # iterating on one module, and wrong the moment you draw a conclusion about the set — hence + # the warning below and the whole-set default. + info "Publishing ${BOLD}${MODULES[*]}${NC} at ${BOLD}$VERSION${NC} to ~/.m2 (unsigned)..." + TASKS=() + for module in "${MODULES[@]}"; do + TASKS+=(":${module}:publishToMavenLocal") + done +fi + +./gradlew "${TASKS[@]}" + +echo +info "================================================" +info " Installed to ~/.m2 — $GROUP at $VERSION" +info "================================================" +echo +if [[ ${#MODULES[@]} -gt 0 ]]; then + warn "Partial install — only these were rebuilt: ${MODULES[*]}. Every other module in ~/.m2" + warn "is whatever was installed last, which may be a different build of the same version." +fi +cat < Date: Tue, 15 Sep 2026 14:10:11 +0100 Subject: [PATCH 14/15] docs(Nav3Navigation): how to deliver a pop result on system back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two sections each told half the story. "System back" says stop wrapping the host in a BackHandler; "Results" says popWithResult hands a value to the screen underneath. Neither says what happens to a screen doing both — and the answer is that system back routes through NavDisplay's onBack to a plain pop(), so the result is dropped. The back arrow keeps working, the gesture silently stops signalling, and nothing errors. A consumer migrating off BackHandler hits this immediately and has no documented landing place. Documents the host-onBack dispatch: an app-side interface read via navigator.lastItem, so NavDisplay still owns the gesture and the predictive pop transition still scrubs. Notes the navigator must be hoisted with rememberBackStackNav3Navigator, since onBack is built at the call site where LocalNav3Navigator is still the outer navigator rather than the one the host provides. Also warns off the obvious wrong fix — an always-enabled NavigationBackHandler — which intercepts ahead of NavDisplay and loses predictive back, the same regression 2bf651e removed from the tabs examples. That API is for conditional interception, not for carrying a payload out of an unconditional pop. Records why this stays app-side rather than becoming a host default or a Nav3Screen.onPopResult hook: an always-popWithResult(null) default would deliver null to receivers that only wanted explicit results, and the hook is this interface with the library guessing the contract instead of the app declaring it. Co-Authored-By: Claude Opus 5 (1M context) --- Nav3Navigation/README.md | 75 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/Nav3Navigation/README.md b/Nav3Navigation/README.md index 9c685d0..2ef77b7 100644 --- a/Nav3Navigation/README.md +++ b/Nav3Navigation/README.md @@ -32,13 +32,13 @@ without giving up the fused-screen / ambient-navigator convenience that Voyager ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:nav3navigation:1.9.1-rc01") +implementation("uk.co.appoly.droid:nav3navigation:1.9.1") ``` Or via the AppolyDroid BOM (version managed by the platform): ```gradle.kts -implementation(platform("uk.co.appoly.droid:bom:1.9.1-rc01")) +implementation(platform("uk.co.appoly.droid:bom:1.9.1")) implementation("uk.co.appoly.droid:nav3navigation") ``` @@ -186,6 +186,10 @@ Bind one `NavigationEventState` to exactly one `NavigationBackHandler` — a sec a state throws `IllegalArgumentException`. Branch inside `onBackCompleted` rather than registering two conditional handlers. +Wanting to hand a **result** back on system back is not a reason to register one — an always-enabled +handler costs you the predictive-back scrub for no interception. See +[Delivering a result on system back](#delivering-a-result-on-system-back). + ### Deep links A deep link is just a seeded start stack — no graph, no URI-pattern framework: @@ -532,6 +536,73 @@ Default decorators include the result-bus decorator. A picker can `sendResult(.. caller observes via `ResultEffect`. **Treat as alpha** — event vs state variants differ on process-death behaviour. Prefer (A) or a shared ViewModel until this hits beta/stable. +#### Delivering a result on system back + +`popWithResult` is **child-initiated**: the screen being popped chooses to deliver. System back +does not go through it — `Nav3ScreenHost` forwards `onBack` to `NavDisplay`, which defaults to +plain `navigator.pop()`. So a screen that hands a value back from its own back arrow delivers +nothing when the user swipes or presses back instead. Nothing fails; the result is simply dropped, +and a "something changed, refresh the list" signal goes missing on the most common exit path. + +**Don't fix this with an always-enabled `NavigationBackHandler`.** It works, and it costs you the +predictive-back animation: the handler intercepts ahead of `NavDisplay`, so `predictivePopTransitionSpec` +never scrubs and the screen no longer animates out under the gesture. See [System back](#system-back) — +that API is for genuinely *conditional* interception, not for "pop, but carry a payload". + +Dispatch from the host's `onBack` instead, on an app-side interface: + +```kotlin +interface PopsWithResult { + fun popResult(): Any? +} + +@Serializable +data class PostDetailScreen(val id: Long) : Nav3Screen, PopsWithResult { + override fun popResult() = true // "something changed, refresh" + + @Composable + override fun Content() { /* back arrow still calls navigator.popWithResult(true) */ } +} +``` + +```kotlin +val backStack = rememberNavBackStack(ListScreen) +// Hoisted: `onBack` is built at the call site, where LocalNav3Navigator is still the *outer* +// navigator (null at the top level) — not the one this host provides to its screens. +val navigator = rememberBackStackNav3Navigator(backStack) + +Nav3ScreenHost( + modifier = Modifier.fillMaxSize(), + backStack = backStack, + navigator = navigator, + onBack = { + when (val top = navigator.lastItem) { + is PopsWithResult -> navigator.popWithResult(top.popResult()) + else -> navigator.pop() + } + }, +) +``` + +Predictive back is untouched — `NavDisplay` still owns the gesture and runs the pop transition; only +what happens on completion changed. Both exits now route through `popWithResult`, so the back arrow +and system back cannot drift apart. + +Three things worth knowing: + +- **`popWithResult` no-ops entirely when `canPop` is `false`** — nothing pops and the result is + dropped. Harmless here, because Nav3 disables its back callback at the root and `onBack` is never + invoked; but don't reuse the helper somewhere a pop at depth 1 is required. +- **Delivery is "pop first, deliver to the revealed top"**, gated on that screen implementing + `Nav3ResultReceiver`. A detail screen reachable from two different lists needs *both* to implement + it — otherwise the pop proceeds and the result is silently dropped. +- **Keep `popResult()` a constant on `@Serializable` keys.** Don't accumulate state on the key to + build a richer result; put the real payload in a screen-scoped ViewModel, same rule as `metadata`. + +This is deliberately app-side. A host default that always called `popWithResult(null)` would hand +`null` to receivers that only wanted results from explicit pops, and a `Nav3Screen.onPopResult` hook +would be this interface with the library guessing the contract instead of the app declaring it. + ### Screen-scoped ViewModels (ScreenModel → ViewModel) Voyager `ScreenModel` + `koinScreenModel()` maps cleanly onto real `ViewModel`s: From c4a7e5ceb9741fc455bcb94f404e65b4bad6918e Mon Sep 17 00:00:00 2001 From: Bradley Duck Date: Tue, 15 Sep 2026 14:10:53 +0100 Subject: [PATCH 15/15] build: TOOLBOX_VERSION 1.9.1-rc01 -> 1.9.1 Drops the release-candidate suffix now the branch is the 1.9.1 release. README version references follow via the UpdateReadmeVersions sync. Co-Authored-By: Claude Opus 5 (1M context) --- AppSnackBar-UiState/README.md | 6 +++--- AppSnackBar/README.md | 2 +- BaseRepo-AppolyJson/README.md | 4 ++-- BaseRepo-Paging-AppolyJson/README.md | 10 +++++----- BaseRepo-Paging/README.md | 8 ++++---- BaseRepo-S3Uploader-Multipart/README.md | 6 +++--- BaseRepo-S3Uploader/README.md | 6 +++--- BaseRepo/README.md | 2 +- ComposeExtensions/README.md | 2 +- ConnectivityMonitor/README.md | 2 +- DateHelperUtil-Room/README.md | 4 ++-- DateHelperUtil-Serialization/README.md | 4 ++-- DateHelperUtil/README.md | 2 +- LazyGridPagingExtensions/README.md | 4 ++-- LazyListPagingExtensions/README.md | 4 ++-- MockInterceptor-AppolyJson/README.md | 2 +- MockInterceptor-Retrofit/README.md | 2 +- MockInterceptor-Serialization/README.md | 2 +- MockInterceptor/README.md | 2 +- PagingExtensions/README.md | 2 +- README.md | 8 ++++---- S3Uploader-Multipart/README.md | 2 +- S3Uploader/README.md | 2 +- SegmentedControl/README.md | 2 +- UiState/README.md | 2 +- buildSrc/src/main/kotlin/BuildConfig.kt | 2 +- 26 files changed, 47 insertions(+), 47 deletions(-) diff --git a/AppSnackBar-UiState/README.md b/AppSnackBar-UiState/README.md index 3e66462..66264e1 100644 --- a/AppSnackBar-UiState/README.md +++ b/AppSnackBar-UiState/README.md @@ -13,9 +13,9 @@ Integration module that bridges the AppSnackBar and UiState modules, providing a ```gradle.kts // Requires both base modules -implementation("uk.co.appoly.droid:uistate:1.9.1-rc01") -implementation("uk.co.appoly.droid:appsnackbar:1.9.1-rc01") -implementation("uk.co.appoly.droid:appsnackbar-uistate:1.9.1-rc01") +implementation("uk.co.appoly.droid:uistate:1.9.1") +implementation("uk.co.appoly.droid:appsnackbar:1.9.1") +implementation("uk.co.appoly.droid:appsnackbar-uistate:1.9.1") ``` ## Usage diff --git a/AppSnackBar/README.md b/AppSnackBar/README.md index 7508ca4..7db2e56 100644 --- a/AppSnackBar/README.md +++ b/AppSnackBar/README.md @@ -13,7 +13,7 @@ A customizable Jetpack Compose Snackbar implementation with support for differen ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:appsnackbar:1.9.1-rc01") +implementation("uk.co.appoly.droid:appsnackbar:1.9.1") ``` ## Usage diff --git a/BaseRepo-AppolyJson/README.md b/BaseRepo-AppolyJson/README.md index 1c8e78d..b34b81d 100644 --- a/BaseRepo-AppolyJson/README.md +++ b/BaseRepo-AppolyJson/README.md @@ -14,8 +14,8 @@ Appoly's JSON format. ```gradle.kts // Requires the base BaseRepo module -implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") -implementation("uk.co.appoly.droid:baserepo-appolyjson:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo:1.9.1") +implementation("uk.co.appoly.droid:baserepo-appolyjson:1.9.1") ``` ## API Response Structure diff --git a/BaseRepo-Paging-AppolyJson/README.md b/BaseRepo-Paging-AppolyJson/README.md index bf8e873..7890f83 100644 --- a/BaseRepo-Paging-AppolyJson/README.md +++ b/BaseRepo-Paging-AppolyJson/README.md @@ -15,13 +15,13 @@ follow Appoly's paging format. ```gradle.kts // Requires the base modules -implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") -implementation("uk.co.appoly.droid:baserepo-paging:1.9.1-rc01") -implementation("uk.co.appoly.droid:baserepo-paging-appolyjson:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo:1.9.1") +implementation("uk.co.appoly.droid:baserepo-paging:1.9.1") +implementation("uk.co.appoly.droid:baserepo-paging-appolyjson:1.9.1") // For Compose UI integration -implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.1-rc01") // For LazyColumn -implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.1-rc01") // For LazyGrid +implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.1") // For LazyColumn +implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.1") // For LazyGrid ``` ## API Response Format diff --git a/BaseRepo-Paging/README.md b/BaseRepo-Paging/README.md index 9bacbe0..b8dd1c7 100644 --- a/BaseRepo-Paging/README.md +++ b/BaseRepo-Paging/README.md @@ -17,12 +17,12 @@ extended for specific JSON formats. ```gradle.kts // Requires the base BaseRepo module -implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") -implementation("uk.co.appoly.droid:baserepo-paging:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo:1.9.1") +implementation("uk.co.appoly.droid:baserepo-paging:1.9.1") // For Compose UI integration -implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.1-rc01") // For LazyColumn -implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.1-rc01") // For LazyGrid +implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.1") // For LazyColumn +implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.1") // For LazyGrid ``` ## Extensions diff --git a/BaseRepo-S3Uploader-Multipart/README.md b/BaseRepo-S3Uploader-Multipart/README.md index c197fdc..d3c1724 100644 --- a/BaseRepo-S3Uploader-Multipart/README.md +++ b/BaseRepo-S3Uploader-Multipart/README.md @@ -15,9 +15,9 @@ Extension module that bridges BaseRepo and S3Uploader-Multipart, enabling pausab ```gradle.kts // Requires the base modules -implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") -implementation("uk.co.appoly.droid:s3uploader-multipart:1.9.1-rc01") -implementation("uk.co.appoly.droid:baserepo-s3uploader-multipart:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo:1.9.1") +implementation("uk.co.appoly.droid:s3uploader-multipart:1.9.1") +implementation("uk.co.appoly.droid:baserepo-s3uploader-multipart:1.9.1") ``` ## Usage diff --git a/BaseRepo-S3Uploader/README.md b/BaseRepo-S3Uploader/README.md index 2a00fe2..fdb67ca 100644 --- a/BaseRepo-S3Uploader/README.md +++ b/BaseRepo-S3Uploader/README.md @@ -18,9 +18,9 @@ An extension module that bridges BaseRepo and S3Uploader, enabling seamless file ```gradle.kts // Requires both the base modules -implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") -implementation("uk.co.appoly.droid:s3uploader:1.9.1-rc01") -implementation("uk.co.appoly.droid:baserepo-s3uploader:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo:1.9.1") +implementation("uk.co.appoly.droid:s3uploader:1.9.1") +implementation("uk.co.appoly.droid:baserepo-s3uploader:1.9.1") ``` ## How it Works diff --git a/BaseRepo/README.md b/BaseRepo/README.md index 9b9ffc3..1061378 100644 --- a/BaseRepo/README.md +++ b/BaseRepo/README.md @@ -14,7 +14,7 @@ Foundation module for implementing the repository pattern with standardized API ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:baserepo:1.9.1-rc01") +implementation("uk.co.appoly.droid:baserepo:1.9.1") ``` ## Extensions diff --git a/ComposeExtensions/README.md b/ComposeExtensions/README.md index 6387b4c..688eb43 100644 --- a/ComposeExtensions/README.md +++ b/ComposeExtensions/README.md @@ -13,7 +13,7 @@ Compose utilities for insets/IME padding, padding arithmetic, serialization-safe ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:composeextensions:1.9.1-rc01") +implementation("uk.co.appoly.droid:composeextensions:1.9.1") ``` ## Usage diff --git a/ConnectivityMonitor/README.md b/ConnectivityMonitor/README.md index 0992c68..cf9418f 100644 --- a/ConnectivityMonitor/README.md +++ b/ConnectivityMonitor/README.md @@ -9,7 +9,7 @@ Add the following dependency to your project's `build.gradle` file: ```gradle.kts -implementation("uk.co.appoly.droid:connectivitymonitor:1.9.1-rc01") +implementation("uk.co.appoly.droid:connectivitymonitor:1.9.1") ``` ## Usage diff --git a/DateHelperUtil-Room/README.md b/DateHelperUtil-Room/README.md index 364e7c0..a738f28 100644 --- a/DateHelperUtil-Room/README.md +++ b/DateHelperUtil-Room/README.md @@ -16,8 +16,8 @@ Extension module for DateHelperUtil that provides Room database integration for ```gradle.kts // Requires base DateHelperUtil module -implementation("uk.co.appoly.droid:datehelperutil:1.9.1-rc01") -implementation("uk.co.appoly.droid:datehelperutil-room:1.9.1-rc01") +implementation("uk.co.appoly.droid:datehelperutil:1.9.1") +implementation("uk.co.appoly.droid:datehelperutil-room:1.9.1") // Required Room dependencies implementation("androidx.room:room-runtime:2.8.5") diff --git a/DateHelperUtil-Serialization/README.md b/DateHelperUtil-Serialization/README.md index f02ede8..7db5188 100644 --- a/DateHelperUtil-Serialization/README.md +++ b/DateHelperUtil-Serialization/README.md @@ -16,8 +16,8 @@ Extension module for DateHelperUtil that provides kotlinx.serialization integrat ```gradle.kts // Requires base DateHelperUtil module -implementation("uk.co.appoly.droid:datehelperutil:1.9.1-rc01") -implementation("uk.co.appoly.droid:datehelperutil-serialization:1.9.1-rc01") +implementation("uk.co.appoly.droid:datehelperutil:1.9.1") +implementation("uk.co.appoly.droid:datehelperutil-serialization:1.9.1") // Required kotlinx.serialization dependencies implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0") diff --git a/DateHelperUtil/README.md b/DateHelperUtil/README.md index 6128c51..37af5c7 100644 --- a/DateHelperUtil/README.md +++ b/DateHelperUtil/README.md @@ -14,7 +14,7 @@ A utility module for standardized date and time operations in Android applicatio ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:datehelperutil:1.9.1-rc01") +implementation("uk.co.appoly.droid:datehelperutil:1.9.1") ``` ## 1.4.1 patch note diff --git a/LazyGridPagingExtensions/README.md b/LazyGridPagingExtensions/README.md index 5f9b564..54abf19 100644 --- a/LazyGridPagingExtensions/README.md +++ b/LazyGridPagingExtensions/README.md @@ -15,8 +15,8 @@ Extension functions for integrating Jetpack Paging 3 with Compose LazyVerticalGr ```gradle.kts // Requires the base PagingExtensions module -implementation("uk.co.appoly.droid:pagingextensions:1.9.1-rc01") -implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.1-rc01") +implementation("uk.co.appoly.droid:pagingextensions:1.9.1") +implementation("uk.co.appoly.droid:lazygridpagingextensions:1.9.1") // Make sure to include Jetpack Paging Compose implementation("androidx.paging:paging-compose:3.5.1") diff --git a/LazyListPagingExtensions/README.md b/LazyListPagingExtensions/README.md index 9cffee0..23738e9 100644 --- a/LazyListPagingExtensions/README.md +++ b/LazyListPagingExtensions/README.md @@ -15,8 +15,8 @@ Extension functions for easy integration of Jetpack Paging 3 with Compose LazyCo ```gradle.kts // Requires the base PagingExtensions module -implementation("uk.co.appoly.droid:pagingextensions:1.9.1-rc01") -implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.1-rc01") +implementation("uk.co.appoly.droid:pagingextensions:1.9.1") +implementation("uk.co.appoly.droid:lazylistpagingextensions:1.9.1") // Make sure to include Jetpack Paging Compose implementation("androidx.paging:paging-compose:3.5.1") diff --git a/MockInterceptor-AppolyJson/README.md b/MockInterceptor-AppolyJson/README.md index 74149b5..3d52bef 100644 --- a/MockInterceptor-AppolyJson/README.md +++ b/MockInterceptor-AppolyJson/README.md @@ -14,7 +14,7 @@ Extension for [MockInterceptor-Serialization](../MockInterceptor-Serialization/) ```gradle.kts // MockInterceptor and MockInterceptor-Serialization are included transitively -implementation("uk.co.appoly.droid:mockinterceptor-appolyjson:1.9.1-rc01") +implementation("uk.co.appoly.droid:mockinterceptor-appolyjson:1.9.1") ``` ## Usage diff --git a/MockInterceptor-Retrofit/README.md b/MockInterceptor-Retrofit/README.md index 6e0949e..2ee209f 100644 --- a/MockInterceptor-Retrofit/README.md +++ b/MockInterceptor-Retrofit/README.md @@ -13,7 +13,7 @@ Extension for [MockInterceptor](../MockInterceptor/) that reads Retrofit HTTP an ```gradle.kts // MockInterceptor is included transitively -implementation("uk.co.appoly.droid:mockinterceptor-retrofit:1.9.1-rc01") +implementation("uk.co.appoly.droid:mockinterceptor-retrofit:1.9.1") ``` > **Note:** Retrofit is a `compileOnly` dependency — your project must already depend on Retrofit. diff --git a/MockInterceptor-Serialization/README.md b/MockInterceptor-Serialization/README.md index c970e7e..d9cc6ec 100644 --- a/MockInterceptor-Serialization/README.md +++ b/MockInterceptor-Serialization/README.md @@ -12,7 +12,7 @@ Extension for [MockInterceptor](../MockInterceptor/) that adds type-safe JSON re ```gradle.kts // MockInterceptor is included transitively -implementation("uk.co.appoly.droid:mockinterceptor-serialization:1.9.1-rc01") +implementation("uk.co.appoly.droid:mockinterceptor-serialization:1.9.1") ``` ## Usage diff --git a/MockInterceptor/README.md b/MockInterceptor/README.md index 9642f7b..9885716 100644 --- a/MockInterceptor/README.md +++ b/MockInterceptor/README.md @@ -16,7 +16,7 @@ An OkHttp interceptor with a route-matching DSL for mocking API responses during ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:mockinterceptor:1.9.1-rc01") +implementation("uk.co.appoly.droid:mockinterceptor:1.9.1") ``` ## Usage diff --git a/PagingExtensions/README.md b/PagingExtensions/README.md index 7686a30..effab19 100644 --- a/PagingExtensions/README.md +++ b/PagingExtensions/README.md @@ -12,7 +12,7 @@ Core utilities and extensions for Jetpack Paging 3 integration, providing the fo ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:pagingextensions:1.9.1-rc01") +implementation("uk.co.appoly.droid:pagingextensions:1.9.1") ``` ## Usage diff --git a/README.md b/README.md index 1b88fbc..f842695 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ In your `libs.versions.toml` file: ```toml [versions] -appolydroidToolbox = "1.9.1-rc01" # Replace with the latest version +appolydroidToolbox = "1.9.1" # Replace with the latest version [libraries] appolydroid-toolbox-bom = { group = "uk.co.appoly.droid", name = "bom", version.ref = "appolydroidToolbox" } @@ -129,7 +129,7 @@ In your module's `build.gradle.kts`: ```gradle.kts dependencies { // Import the BOM - implementation(platform("uk.co.appoly.droid:bom:1.9.1-rc01")) + implementation(platform("uk.co.appoly.droid:bom:1.9.1")) // Now you can use AppolyDroid modules without specifying versions implementation("uk.co.appoly.droid:baserepo") @@ -166,7 +166,7 @@ In your `libs.versions.toml` file: ```toml [versions] -appolydroidToolbox = "1.9.1-rc01" # Replace with the latest version +appolydroidToolbox = "1.9.1" # Replace with the latest version [libraries] #AppolyDroid-Toolbox @@ -234,7 +234,7 @@ In your module's `build.gradle.kts`: ```gradle.kts dependencies { - val appolydroidToolbox = "1.9.1-rc01" // Replace with the latest version + val appolydroidToolbox = "1.9.1" // Replace with the latest version // Add only the modules you need implementation("uk.co.appoly.droid:baserepo:$appolydroidToolbox") implementation("uk.co.appoly.droid:baserepo-appolyjson:$appolydroidToolbox") diff --git a/S3Uploader-Multipart/README.md b/S3Uploader-Multipart/README.md index db3835e..5147a01 100644 --- a/S3Uploader-Multipart/README.md +++ b/S3Uploader-Multipart/README.md @@ -16,7 +16,7 @@ Advanced S3 upload module with pause, resume, and recovery support using AWS S3 ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:s3uploader-multipart:1.9.1-rc01") +implementation("uk.co.appoly.droid:s3uploader-multipart:1.9.1") ``` This module depends on `S3Uploader` and includes it transitively. diff --git a/S3Uploader/README.md b/S3Uploader/README.md index 4918598..100276d 100644 --- a/S3Uploader/README.md +++ b/S3Uploader/README.md @@ -16,7 +16,7 @@ Standalone module for Amazon S3 file uploading with progress tracking and error ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:s3uploader:1.9.1-rc01") +implementation("uk.co.appoly.droid:s3uploader:1.9.1") ``` ## Usage diff --git a/SegmentedControl/README.md b/SegmentedControl/README.md index 255ebd6..146b4c0 100644 --- a/SegmentedControl/README.md +++ b/SegmentedControl/README.md @@ -17,7 +17,7 @@ A highly customizable iOS-style segmented control for Jetpack Compose with smoot ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:segmentedcontrol:1.9.1-rc01") +implementation("uk.co.appoly.droid:segmentedcontrol:1.9.1") ``` ## Usage diff --git a/UiState/README.md b/UiState/README.md index 6649af5..b1e0c53 100644 --- a/UiState/README.md +++ b/UiState/README.md @@ -13,7 +13,7 @@ A standardized UI state management library for Android applications, providing c ## Installation ```gradle.kts -implementation("uk.co.appoly.droid:uistate:1.9.1-rc01") +implementation("uk.co.appoly.droid:uistate:1.9.1") ``` ## Usage diff --git a/buildSrc/src/main/kotlin/BuildConfig.kt b/buildSrc/src/main/kotlin/BuildConfig.kt index 2bc7b19..377aa1c 100644 --- a/buildSrc/src/main/kotlin/BuildConfig.kt +++ b/buildSrc/src/main/kotlin/BuildConfig.kt @@ -9,7 +9,7 @@ object BuildConfig { * The current version of the AppolyDroid Toolbox library. * This is used for maven publishing and README version updates. */ - const val TOOLBOX_VERSION = "1.9.1-rc01" + const val TOOLBOX_VERSION = "1.9.1" /** * SDK version configuration for Android modules.