Skip to main content

guided_tutor 0.2.0 copy "guided_tutor: ^0.2.0" to clipboard
guided_tutor: ^0.2.0 copied to clipboard

A dependency-free guided-tour overlay for Flutter: a scrim spotlights one widget at a time with a caption explaining it.

guided_tutor #

pub package license: MIT

Preview

A hand-rolled, dependency-free guided tour overlay for Flutter: a scrim with one widget spotlighted through it and a caption explaining it, stepping through your screen — including scrolling to targets that start off-screen — one step at a time. The GIF above is a real tour, from a real app, running on this package.

No state-management dependency, no bundled localisation: you own the copy, the step list, and whatever you do with "has this been seen" — the package is just the overlay.

Why #

Most "onboarding tour" packages come with an opinionated look, their own localisation story, and often a state-management dependency you didn't ask for. guided_tutor is the opposite bet: it's a single, self-contained widget, styled from your app's own Theme by default, that:

  • skips steps whose target isn't currently in the tree, instead of drawing a spotlight around nothing — so the same step list works for an empty state and a screen full of data, and the step counter reports what is actually being shown, not the length of your list;
  • scrolls a target into view before measuring it, waiting a frame for layout and a frame for the scroll to settle, which is the difference between a spotlight that lands on the widget and one that lands a few hundred pixels away from it;
  • never traps the user — a "Skip" link is on screen on every step, the system back gesture leaves the tour rather than the screen under it, and only the caption's explanation scrolls, never its Back/Next controls off the bottom of the card.

Get started in under a minute #

This is the entire tutorial for the common case. Three steps: key it, describe it, show it.

1. Give the widgets you want to spotlight a GlobalKey #

final _headerKey = GlobalKey();
final _fabKey = GlobalKey();

// ... later, in build():
AppBar(key: _headerKey, title: const Text('Home')),
FloatingActionButton(key: _fabKey, onPressed: ..., child: const Icon(Icons.add)),

2. Describe the steps — a target key, a title, and a body, each #

final steps = [
  const GuidedTourStep(
    title: 'Welcome',
    body: "This step has no target key, so it's about the screen as a "
        'whole rather than one widget on it.',
  ),
  GuidedTourStep(
    targetKey: _headerKey,
    title: 'The header',
    body: 'Anything with a GlobalKey can be spotlighted.',
  ),
  GuidedTourStep(
    targetKey: _fabKey,
    title: 'Add something',
    body: 'Tap here to create your first item.',
    shape: GuidedTourShape.circle, // round target, round hole
  ),
];

That's it for content — no labels, no locale, no shape you have to think about beyond the one circular target above.

3. Show it #

import 'package:guided_tutor/guided_tutor.dart';

ElevatedButton(
  onPressed: () => GuidedTour.show(context: context, steps: steps),
  child: const Text('Start tour'),
)

GuidedTour.show inserts the tour into the app's root Overlay and removes itself the moment it's done — no Stack, no showTour boolean, no setState to wire up. Next/Back/Skip/Done come from GuidedTourLabels.standard (English) unless you pass your own. Run the tour, tap through it, tap "Skip" — it behaves exactly like the full walkthrough in example/, because it is the same GuidedTour underneath; .show() is just the fast way to host it.

If you want to know when it closes (to persist "seen it"), pass onClose:

GuidedTour.show(
  context: context,
  steps: steps,
  onClose: ({required completed}) => myPrefs.setTourSeen(true),
);

That's the whole tutorial. Everything below is for when you need more.

Going further #

Full control: hosting it yourself #

.show() covers most cases, but sometimes you want the tour's lifetime tied to your own state — to gate it behind a persisted "have they seen this" flag, to control exactly where in the tree it's mounted, or to swap it out on a hot-reload during development. GuidedTour is a plain widget; host it in a Stack instead:

// Stack it over your Scaffold, not inside one — the tour needs to be able
// to draw over everything it describes, a navigation bar included.
Stack(
  children: [
    scaffold,
    if (showTour)
      GuidedTour(
        steps: steps,
        onClose: ({required completed}) => setState(() => showTour = false),
      ),
  ],
)

showTour is a plain bool you own — set it from a "seen the tour before?" check on startup, a Riverpod/Bloc notifier, or just setState. This is exactly what .show() does for you under the hood, minus the OverlayEntry bookkeeping.

Anchors, without your screen needing to know a tour exists #

TourAnchors<T> + TourAnchorScope<T> give you the GlobalKey plumbing for free, generic over your own enum of "everything this tour can point at" — useful once a tour spans more than a couple of widgets, since a flat enum reads as the tour's whole route and a renamed/removed target becomes a compile error instead of a silently-missing step:

enum HomeAnchor { header, list, fab }

class _HomeState extends State<Home> {
  final _anchors = TourAnchors<HomeAnchor>(HomeAnchor.values); // once, as a field

  @override
  Widget build(BuildContext context) {
    return TourAnchorScope<HomeAnchor>(
      anchors: _anchors,
      child: Scaffold(
        appBar: AppBar(
          title: TourAnchorScope.wrap(context, HomeAnchor.header, const Text('Home')),
        ),
        // ...
      ),
    );
  }
}

Then build steps from _anchors[HomeAnchor.header] instead of juggling GlobalKey fields by hand. Outside a TourAnchorScope, wrap/keyFor are no-ops — a widget test or a design-preview index that builds your screen without a tour above it just gets no anchors, quietly, rather than a crash from a shared GlobalKey attached to more than one element at once.

Styling #

Every colour, size, and duration is read from GuidedTourStyle, whose fields are all optional — leave them all unset and the tour reads its colours and text styles from the ambient Theme:

GuidedTour.show(
  context: context,
  steps: steps,
  style: const GuidedTourStyle(
    ruleColor: Colors.amber,
    cardMaxWidth: 360,
  ),
)

See the doc comments on GuidedTourStyle for the full list.

Circular targets #

A rectangular hole around a circular widget (a FAB, an avatar, a ring) lights up its empty corners for no reason. Set shape: GuidedTourShape.circle on that step and the hole is a circle sized to the target's longest side instead.

Your own labels, for any language beyond English #

GuidedTourLabels(
  next: 'Lanjut',
  back: 'Kembali',
  skip: 'Lewati',
  done: 'Selesai',
  // A builder, not a template string: the position it reports is what's
  // actually being shown once absent targets are skipped, not a step's
  // raw index.
  counter: (position, total) => '$position dari $total',
)

Persistence #

Whether a given user has seen the tour, and whether it should open on its own, is deliberately left to you — there's no bundled storage to configure. The usual shape, wherever you already keep app preferences:

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (!myPrefs.hasSeenTour) {
      GuidedTour.show(
        context: context,
        steps: steps,
        onClose: ({required completed}) => myPrefs.setTourSeen(true),
      );
    }
  });
}

Example #

example/ is a runnable app covering a target-less step, an off-screen target the tour scrolls to, a step whose target is conditionally absent, and a circular spotlight — started two ways from the same step list: the AppBar's lightning-bolt icon calls GuidedTour.show, the floating action button hosts GuidedTour in a Stack with its own state, so you can compare both paths side by side.

What this package does not do #

  • No persistence. "Has this user seen the tour" is your app's concern, not this widget's.
  • No bundled copy or localisation beyond GuidedTourLabels.standard's English defaults. Every string is handed in through GuidedTourStep/GuidedTourLabels, already in whatever language you chose.
  • No animation/state-management dependency. GuidedTour is a plain StatefulWidget; wire it up to Riverpod, Bloc, setState, or nothing at all.

License #

MIT. See LICENSE.

1
likes
160
points
69
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A dependency-free guided-tour overlay for Flutter: a scrim spotlights one widget at a time with a caption explaining it.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on guided_tutor