feat(shell): swipe between tabs
The four tabs are now pages of one HorizontalPager on a single NavHost route, so a sideways swipe moves between Alarms, Timers, Stopwatch and World clock as well as tapping the bar. The pager is the source of truth for the selected tab; editors, the timer setup page and Settings stay NavHost destinations pushed over it. A tab tap from one of them pops back to the tabs, and a deep link always lands on its tab rather than on whatever nested screen was last open there. Swiping is on only under a navigation bar: beside a rail the tabs are stacked vertically, and a horizontal swipe has no counterpart there. Closes #2
This commit is contained in:
+42
-17
@@ -1304,27 +1304,46 @@ default rather than writing `if (compact) NavigationBar else NavigationRail` is
|
||||
the point — the default also answers the tabletop posture and the compact-*height*
|
||||
case a hand-rolled branch quietly gets wrong.
|
||||
|
||||
The `NavHost` is the single source of truth for the selected tab. There is no
|
||||
`var selectedTab by rememberSaveable`: the selection is derived from
|
||||
`currentBackStackEntryAsState()`, and Navigation Compose already saves its back
|
||||
stack through a `SavedStateHandle`, so the tab survives rotation and process
|
||||
death with no mirror of ours to drift.
|
||||
**The tabs are pages of one `HorizontalPager` (#2).** Until then each tab was
|
||||
its own `NavHost` route and the back stack was the source of truth for the
|
||||
selected tab; the four tabs could only be tapped. Now they sit on a single
|
||||
route, `ShellNavigation.TABS_ROUTE`, and the **pager** is the single source of
|
||||
truth for the selected tab: its state is saveable, hoisted above the `NavHost`
|
||||
so a tap from an editor can still move it, and the tab survives rotation and
|
||||
process death with no mirror of ours to drift. The `NavHost` keeps only what is
|
||||
pushed over the tabs — the two editors, the timer setup page, Settings. Each tab
|
||||
is composed only while it is on screen (`beyondViewportPageCount` stays 0), keeps
|
||||
its saveable state under its page key while off screen, and its ViewModel in the
|
||||
tabs route's store.
|
||||
|
||||
The policy that back stack follows is a pure function, `ShellNavigation`:
|
||||
Swiping is on only under a navigation **bar**: the bar is a horizontal row of
|
||||
the same four tabs, so a sideways swipe maps onto it one to one. Beside a rail —
|
||||
tablets, unfolded foldables — the tabs are stacked vertically and a sideways
|
||||
swipe has no counterpart the user can see, so there the pager is tap-only. The
|
||||
shell computes `NavigationSuiteScaffoldDefaults.navigationSuiteType` itself and
|
||||
hands the same value to the scaffold, so the two cannot disagree.
|
||||
|
||||
- a tab tap is `popUpTo(start) { saveState = true }` + `launchSingleTop` +
|
||||
`restoreState`; re-selecting the tab you are already on is the same command
|
||||
with `restoreState = false`, which pops that tab's own inner stack back to its
|
||||
root — the standard behaviour, and the contract M5's editor will rely on;
|
||||
- back from any of the other three tabs returns to **Alarms**; back from Alarms,
|
||||
from a null route and from any route the shell does not recognise leaves the
|
||||
app — a nested destination's back belongs to the `NavController`, not to us.
|
||||
The policy the shell follows is a pure function, `ShellNavigation`:
|
||||
|
||||
- a tab tap on the tabs slides the pager there — from the neighbouring page, so
|
||||
a jump across two tabs does not compose and flash past the ones in between;
|
||||
- a tab tap over a nested screen pops back to the tabs first and jumps, and
|
||||
re-selecting the tab under an editor closes the editor — the contract M5's
|
||||
editor relies on. A nested screen is no longer kept per tab and restored when
|
||||
its tab is re-selected: every one writes as the user goes (M5 D22), so popping
|
||||
one loses nothing;
|
||||
- a tab an intent asks for (a notification, a widget, the exported door) is the
|
||||
same pop and a jump — it always lands on the tab itself, never on whatever
|
||||
nested screen was last open there;
|
||||
- back from any of the other three tabs returns to **Alarms**; back from Alarms
|
||||
and from a null route leaves the app, and a nested screen's back belongs to
|
||||
the `NavController`, not to us.
|
||||
|
||||
Predictive back is the kit's `Modifier.predictiveBack`, gated on exactly that
|
||||
policy: on Alarms the handler is *off*, so the gesture falls through to the
|
||||
system and previews leaving the app, which is the truthful preview. Tab switches
|
||||
use the kit's `fadeThrough()` — peer destinations have no spatial relationship,
|
||||
and a slide would claim a hierarchy that is not there.
|
||||
system and previews leaving the app, which is the truthful preview. Nested
|
||||
screens use the kit's `fadeThrough()` — they have no spatial relationship to the
|
||||
tabs, and a slide would claim one.
|
||||
|
||||
### The live pill
|
||||
|
||||
@@ -1439,6 +1458,10 @@ tab stays highlighted while the editor is open; `onBack` of that route is
|
||||
`Exit`, so the shell's predictive-back handler stands aside and the editor pops
|
||||
itself; and a tab tap's `popUpTo(start.route)` pops the editor, which is the
|
||||
right behaviour and exactly the contract §12 wrote down for this milestone.
|
||||
Since the tabs became pager pages (#2) the list lives on the shared tabs route
|
||||
instead, and the same three properties hold through the pager: it keeps Alarms
|
||||
selected under the editor, the shell stands aside on a nested route's back, and
|
||||
a tab tap over the editor pops it.
|
||||
|
||||
There is **no "new alarm" sentinel id**. The FAB creates the alarm — enabled,
|
||||
non-repeating, at the next whole hour, every override null — and then navigates
|
||||
@@ -1940,7 +1963,9 @@ deep link: the content intent carries `TimerIntents.ACTION_SHOW_TIMERS`,
|
||||
`ShellNavigation.onTabSelected` — the same command a tab tap produces.
|
||||
`startDestination` stays **Alarms**: it is the `popUpTo` anchor and the root of
|
||||
M4's asserted back policy, so changing it for a notification tap would rewrite
|
||||
that policy.
|
||||
that policy. (Since #2 the shell replays `ShellNavigation.onTabRequested`
|
||||
instead: a tap's pop, but a jump rather than a slide, onto the pager whose start
|
||||
page is still Alarms — §12.)
|
||||
|
||||
M9 finished that surface and widened it by one step. `tabForAction` gained
|
||||
exactly **one** action, `AlarmIntents.ACTION_SHOW_ALARMS`: the platform's own
|
||||
|
||||
Reference in New Issue
Block a user