Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions packages/kaisel/doc/migration/from-auto-route.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,45 @@ are now separate — the screen takes its parameter directly, no
value equality so two `ProductDetail('sku-42')` instances are equal
to each other (matters for stack diffing).

**Routes that carried callbacks.** A closure field breaks both `const`-ness
and value equality, so `onComplete` / `onClose` / `onAction` can't travel on a
kaisel route. Move the behaviour to the page builder, which has the router in
scope, and turn the callback into a result:

```dart
// route carries data only; the builder wires the behaviour
EddFlow(:final type) => EddFlowPage(type: type, onComplete: router.pop),
```

That works, but a bare `pop()` is indistinguishable from the user backing out
— both arrive as `null` at `pushForResult`. When the caller needs to tell
"completed" from "dismissed", either pop an explicit value
(`onComplete: () => router.pop(true)`) or — for a multi-step sub-experience —
reach for [`run<T>`](/guides/modal-flows/) with a `KaiselModalRoute<T>`, whose
typed completion contract exists for exactly this conversion.

**Parameters inherited from a parent router.** auto_route lets a nested child
read a path parameter declared on its parent via
`@PathParam.inherit('countryCode')`, resolved from the URL at build time — so
the child's generated args class never carries it. kaisel routes are explicit
data with no inheritance: the child must declare the field itself, which also
means **every construction site for that route gains an argument**.

```dart
final class BillProvider extends AppRoute {
const BillProvider({required this.countryCode, required this.providerId});
final String countryCode;
final String providerId;
@override
List<Object?> get props => [countryCode, providerId];
}
```

The change is mechanical but nothing in the type system points at it — a naive
port compiles and then has no idea what country it is for. Grep for
`PathParam.inherit` before you start; each hit is a field to add and a set of
call sites to update.

Group your routes into a single sealed hierarchy:

```dart
Expand Down
17 changes: 17 additions & 0 deletions skills/kaisel/SHELLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,23 @@ branch shares **one** route type `R`, so there's no per-branch typing.
It builds its own `ShellRouter` internally from `branchInitials` — you
don't construct one, and there's no `shell:` parameter.

**A screen that is both a tab and a full screen needs two route values.**
This follows from the scoping rule below, and it looks like duplication until
you see why. A "recipients" screen might be a bottom-nav tab (no app bar of
its own — the shell chrome supplies one) *and* be reachable as a full screen
from a settings drawer (its own app bar, covering the bottom bar). Those are
genuinely different navigation states, so each family gets a value:

```dart
final class RecipientsTab extends DashboardTabRoute { const RecipientsTab(); }
final class Recipients extends AppRoute { const Recipients(); }
```

Both build the same page widget with different chrome. The alternative —
one value in two families — isn't expressible, and shouldn't be: pushing the
tab's value onto the main stack would render a tab body with no shell around
it.

**Critical: `R` must be a sealed type scoped to the shell's routes, not
your app-wide `AppRoute`.** The `pageBuilder` switch is exhaustive over
`R`; if `R` is `AppRoute`, the switch has to handle *every* route the app
Expand Down
Loading