guided_flow

pub package

Interactive guided tours for Flutter — the highlighted widget stays tappable, and the tour follows the user across sheets and routes.

Most tour packages dim the screen and ask the user to press Next. The user reads a slideshow and learns nothing by doing. guided_flow turns that around: the spotlight has a real hole, the button underneath still works, and the tour moves forward when the user actually performs the step.

  • Tap-through spotlight — the real onTap fires. Taps outside the hole are absorbed, so a bottom sheet cannot be swiped shut mid-tour.
  • Survives route changes — the overlay lives above the Navigator, so bottom sheets, dialogs and pushed pages are spotlighted too.
  • Moves by itself — steps follow which anchors are on screen. You never call next() by hand, and backing out of a sheet rewinds the tour with the user.
  • Small surface — three classes to learn, one line to install.

Screenshots

Step 1 — on the page Step 2 — inside a bottom sheet
Step 1 of 10: the spotlight is on the dock button on the home page, and the card asks the user to tap it Step 2 of 10: the bottom sheet has opened by itself and the spotlight has moved to a card inside it

The user tapped the real dock button on the left, which opened the bottom sheet on the right — the tour moved to step 2 on its own. Both shots use a custom GuidedFlowTheme (accentColor, skipLabel: 'Lewati', a progressLabel of PANDUAN · n DARI 10).

Install

dependencies:
  guided_flow: ^1.0.0

Getting started

1. Describe the tour.

final tour = GuidedFlowController(
  steps: const [
    GuidedFlowStep(
      anchorId: 'add-button',
      title: 'Start here',
      body: 'Every expense begins with this button.',
      actionLabel: 'Tap the + button',
      shape: BoxShape.circle,
    ),
    GuidedFlowStep(
      anchorId: 'save',
      title: 'Save it',
      body: 'Tapping this really saves — the tour never stood in the way.',
      actionLabel: 'Tap Save',
      endsTour: true,
    ),
  ],
  onFinish: (ending) => debugPrint('Tour ended: ${ending.name}'),
);

2. Install the host once, above the Navigator.

MaterialApp(
  builder: (context, child) => GuidedFlow(controller: tour, child: child!),
  home: const HomePage(),
)

MaterialApp.builder is the important part. Anything below the Navigator would be buried by the next route you push.

3. Wrap the widgets the steps point at.

GuidedFlowAnchor(
  id: 'save',
  child: FilledButton(onPressed: _save, child: const Text('Save')),
)

GuidedFlowAnchor adds no render object, no padding and no gesture, so your layout is untouched. Without a GuidedFlow above it, it does nothing at all — safe to leave in place.

4. Start when you are ready.

tour.start();

Nothing is drawn until the first anchor is on screen, so it is fine to start early.

How the tour moves

You will rarely call next(). Three rules keep the tour in step with the user:

What happens on screen What the tour does
The next step's anchor appears Moves forward — the user just finished this step
The current anchor leaves while the next one is already there Moves forward
The current anchor leaves and nothing follows it After rewindDelay (1.2 s), goes back to the last step still on screen

The delay is what stops a route transition from being mistaken for the user backing out. While the app is in the background — a camera, a file picker, a share sheet — the tour waits instead of rewinding.

Two flags cover the awkward cases:

  • waitForButton: true — the step never moves on by itself; the card grows a button instead. Use it when two steps live on the same screen, otherwise the second anchor is already there and the tour skips straight past the first.
  • canRewind: false — past a point of no return. If the flow breaks there, the tour stops rather than jumping back to the beginning.

Numbering

By default a step is numbered by its position. Give several steps the same number to present them as one visible step — useful when one screen needs two or three spotlights in a row:

GuidedFlowStep(anchorId: 'note-field',  number: 3, ...),
GuidedFlowStep(anchorId: 'note-picker', number: 3, ...),
GuidedFlowStep(anchorId: 'note-submit', number: 3, ...),

The user sees "step 3 of N" for all three.

Theming

GuidedFlow(
  controller: tour,
  theme: const GuidedFlowTheme(
    accentColor: Color(0xFF10B981),
    skipLabel: 'Lewati',
    progressLabel: myProgressLabel, // (number, total) => 'LANGKAH $number/$total'
  ),
  child: child!,
)

For a card of your own, pass cardBuilder and build whatever you like from the GuidedFlowCardData you are handed.

Showing it only once

The package does not persist anything — that choice is yours. A common shape:

Future<void> maybeStartTour() async {
  final prefs = await SharedPreferences.getInstance();
  if (prefs.getBool('tour_done') ?? false) return;
  tour.start();
}

// in the controller:
onFinish: (ending) {
  if (ending == GuidedFlowEnding.stopped) return; // the flow broke; try again later
  SharedPreferences.getInstance().then((p) => p.setBool('tour_done', true));
}

GuidedFlowEnding tells you whether the user completed the tour, skipped it, or whether it stopped because the flow broke.

Notes

  • Use each anchor id once at a time. Two live anchors with the same id would fight over where the spotlight goes.
  • A tour needs at least one step, and one GuidedFlow host per controller.
  • The overlay repaints every frame while a tour runs, so pumpAndSettle never settles in widget tests. Use pump(Duration) instead.

Example

example/ walks a four-step tour across a page, a bottom sheet and a pushed page — the shortest way to see all of the above working together.

License

MIT

Libraries

guided_flow
Interactive guided tours for Flutter.