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.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:guided_tutor/guided_tutor.dart';

void main() => runApp(const DemoApp());

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'guided_tutor example',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      darkTheme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        brightness: Brightness.dark,
        useMaterial3: true,
      ),
      home: const HomeScreen(),
    );
  }
}

/// Everything the tour on this screen can point at. One flat enum, so a
/// target that gets renamed or removed is a compile error in both the widget
/// that owns it and the step list that points at it.
enum HomeAnchor { header, optionalCard, farListItem, fab }

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  // Created once as a field, not in build(), so the keys keep their identity
  // across rebuilds.
  final _anchors = TourAnchors<HomeAnchor>(HomeAnchor.values);

  bool _showTour = false;

  // Toggled from the switch below. With this off, HomeAnchor.optionalCard is
  // never mounted — and the tour step that points at it is skipped
  // automatically rather than the app needing to special-case it.
  bool _showOptionalCard = true;

  List<GuidedTourStep> _steps() => [
    const GuidedTourStep(
      title: 'Welcome',
      body:
          "This step has no target — it's about the screen as a "
          'whole, so the whole screen just dims.',
    ),
    GuidedTourStep(
      targetKey: _anchors[HomeAnchor.header],
      title: 'The header',
      body: 'A plain widget, spotlighted with a rounded-rectangle hole.',
    ),
    GuidedTourStep(
      targetKey: _anchors[HomeAnchor.optionalCard],
      title: 'An optional card',
      body:
          "Turn its switch off and this step vanishes on its own — "
          "there's nothing here for the tour to skip past manually.",
    ),
    GuidedTourStep(
      targetKey: _anchors[HomeAnchor.farListItem],
      title: 'Scrolled into view',
      body:
          'This item starts off-screen. The tour scrolled the list to '
          'bring it into view before measuring it — try it on a short '
          'window to see the scroll happen.',
      alignment: 0.3,
    ),
    GuidedTourStep(
      targetKey: _anchors[HomeAnchor.fab],
      title: 'A circular spotlight',
      body:
          'Round targets get a round hole, sized to their longest '
          'side, instead of a rectangle with empty corners lit up.',
      shape: GuidedTourShape.circle,
    ),
    const GuidedTourStep(
      title: "That's it",
      body:
          'Skip is always on screen, on every step — a tour nobody '
          'can leave is a trap, not a tutorial.',
    ),
  ];

  @override
  Widget build(BuildContext context) {
    final scaffold = Scaffold(
      appBar: AppBar(
        title: TourAnchorScope.wrap(
          context,
          HomeAnchor.header,
          const Text('guided_tutor example'),
        ),
        actions: [
          // The lite path: no Stack, no showTour state, no labels — just the
          // same steps handed to GuidedTour.show(). Compare with the FAB
          // below, which drives the exact same tour the long way.
          IconButton(
            tooltip: 'Quick tour (lite)',
            icon: const Icon(Icons.flash_on),
            onPressed: () => GuidedTour.show(context: context, steps: _steps()),
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          const Text(
            'Scroll down, then tap "Show tour" to see the guided tours walk '
            'through this screen, including scrolling to a target that '
            "starts off-screen.",
          ),
          const SizedBox(height: 16),
          SwitchListTile(
            title: const Text('Show optional card'),
            subtitle: const Text(
              'Off = its tour step is skipped, not shown empty',
            ),
            value: _showOptionalCard,
            onChanged: (v) => setState(() => _showOptionalCard = v),
          ),
          if (_showOptionalCard)
            TourAnchorScope.wrap(
              context,
              HomeAnchor.optionalCard,
              Card(
                child: ListTile(
                  leading: const Icon(Icons.star),
                  title: const Text('Optional card'),
                  subtitle: const Text(
                    'Only in the tree when the switch above is on',
                  ),
                ),
              ),
            ),
          const SizedBox(height: 16),
          for (var i = 0; i < 12; i++)
            ListTile(
              key: i == 10
                  ? TourAnchorScope.keyFor(context, HomeAnchor.farListItem)
                  : null,
              leading: CircleAvatar(child: Text('${i + 1}')),
              title: Text('List item ${i + 1}'),
            ),
        ],
      ),
      // The full-control path: hosted in the Stack below, with its own
      // showTour flag and custom labels. Same tour as the AppBar's lite
      // button above, wired up the long way — use this when you need to
      // override the style, localise the chrome, or control exactly where
      // in the tree the overlay lives.
      floatingActionButton: TourAnchorScope.wrap(
        context,
        HomeAnchor.fab,
        FloatingActionButton(
          tooltip: 'Full-control tour',
          onPressed: () => setState(() => _showTour = true),
          child: const Icon(Icons.school),
        ),
      ),
    );

    return TourAnchorScope<HomeAnchor>(
      anchors: _anchors,
      child: Stack(
        children: [
          scaffold,
          if (_showTour)
            GuidedTour(
              steps: _steps(),
              labels: GuidedTourLabels(
                next: 'Next',
                back: 'Back',
                skip: 'Skip',
                done: 'Done',
                counter: (position, total) => '$position of $total',
              ),
              onClose: ({required completed}) =>
                  setState(() => _showTour = false),
            ),
        ],
      ),
    );
  }
}
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