Introduce Me banner

Introduce Me

Flutter Dart Version License Platform

A flexible, lightweight Flutter showcase & onboarding package.
Smart tour engine, auto-discovery, and lean rendering — built for real apps.

Introduce Me example app demo


Table of Contents


Why Introduce Me?

Capability Introduce Me
Step setup Auto-discovery by id + order — no GlobalKey lists
Cross-screen tours Queue-based auto-resume across routes
Persistence Built-in TourStorage
Rendering Single OverlayPortal + scoped repaint
Lazy lists Scrollable.ensureVisible scroll-then-measure
Analytics Granular funnel hooks
Multi-highlight groupId + isPrimary
Tooltip actions Configurable buttons without custom widgets
Accessibility enableSemantics — screen-reader labels on hit areas + tooltip
Resilience skipIfTargetNotPresent — tours skip missing/off-screen targets
Auto-scroll enableAutoScroll toggle — opt out for manual scroll control

Installation

dependencies:
  introduceme: ^1.0.0
flutter pub get
import 'package:introduceme/introduceme.dart';

Quick Start

1. Wrap your app

Place IntroduceMeScopeWidget above the screens that contain showcase targets (usually around MaterialApp home, or as a parent of your shell).

void main() {
  runApp(
    MaterialApp(
      home: IntroduceMeScopeWidget(
        scopeName: 'main',
        storage: SharedPrefsTourStorage(), // or MemoryTourStorage() for tests
        child: const HomePage(),
      ),
    ),
  );
}

2. Tag targets

Showcase.auto(
  id: 'menu-button',
  tourId: 'onboarding',
  order: 1,
  title: 'Menu',
  description: 'Access all features from here.',
  child: IconButton(
    icon: const Icon(Icons.menu),
    onPressed: () {},
  ),
)

3. Start the tour

IntroduceMeScope.of(context).start('onboarding');

No GlobalKey list required — steps register themselves via id + order + tourId.


Feature Overview

Area What you get
Auto-discovery Showcase.auto / .introduceMe(auto: true)
Tooltip Title, description, arrow, 5 placements, custom actions
Custom UI Showcase.withWidget + TooltipController
Multi-showcase Highlight several widgets in one step
Overlay Barrier color/opacity, blur, glass tooltip
Gestures Tap / double-tap / long-press / barrier click
Scroll Auto-scroll to off-screen targets + loading widget
Persistence Skip completed tours automatically
Analytics Step shown/skipped/completed, tour abandoned/finished
Theme IntroduceMeTheme as ThemeExtension
Scopes Named scopes for context-free access
Perf Throttled tracking, coalesced rebuilds, costly features opt-in
Accessibility enableSemantics (default true) on overlay hit areas + tooltip
Resilience skipIfTargetNotPresent + missingTargetTimeout
Auto-scroll enableAutoScroll scope/step toggle
Dismiss reasons onDismiss(tourId, TourDismissReason)
Action styling Per-button colors/styles, hideWhen, TooltipActionPosition
Look presets ShowcaseLook — material, ios, glass, neumorphism, retro, neoBrutalism

Core Concepts

Term Meaning
Scope IntroduceMeScopeWidget — owns registry, controller, and overlay
Tour Named sequence of steps (tourId, e.g. 'onboarding')
Step One showcase moment, ordered by order
Target Widget wrapped by Showcase / Showcase.auto
Group Multiple targets highlighted together (groupId)
Primary Group member that owns the tooltip (isPrimary: true)

Tour flow:

start(tourId)
  → resolve steps by order
  → scroll if needed (waitingForTarget)
  → show overlay + tooltip
  → next / skip / dismiss / complete

Tagging Targets

Self-registers when mounted. Best for most apps.

Showcase.auto(
  id: 'fab-add',
  tourId: 'onboarding',
  order: 2,
  title: 'Quick Add',
  description: 'Create something new instantly.',
  targetShapeBorder: const CircleBorder(),
  showArrow: true,
  tooltipPlacement: TooltipPlacement.above,
  child: FloatingActionButton(
    onPressed: () {},
    child: const Icon(Icons.add),
  ),
)

Required: id, tourId, order, child.

Optional per-step overrides: enableSemantics (nullable, inherits scope default true), enableAutoScroll (nullable, inherits scope default true), skipIfTargetNotPresent, look, and all other styling fields.

One-line extension

Icon(Icons.search).introduceMe(
  id: 'search',
  tourId: 'onboarding',
  order: 2,
  title: 'Search',
  description: 'Find anything instantly.',
  auto: true,
)

For advanced options (actions, blur, groupId, etc.), prefer Showcase.auto directly.

Showcase (manual)

Same styling API as .auto, useful when you already manage keys or pass a full ShowcaseStepConfig.

Showcase(
  id: 'avatar',
  tourId: 'onboarding',
  order: 6,
  title: 'Your Avatar',
  description: 'Tap to change your profile picture.',
  targetShapeBorder: const CircleBorder(),
  child: const CircleAvatar(radius: 48, child: Icon(Icons.person)),
)

Showcase.withWidget (fully custom tooltip)

Showcase.withWidget(
  id: 'premium',
  tourId: 'onboarding',
  order: 7,
  container: (context, controller) {
    return Material(
      borderRadius: BorderRadius.circular(16),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('Custom Tooltip', style: TextStyle(fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            const Text('Full control over layout and branding.'),
            const SizedBox(height: 16),
            Align(
              alignment: Alignment.centerRight,
              child: FilledButton(
                onPressed: controller.onNext,
                child: Text(controller.isLast ? 'Done' : 'Next'),
              ),
            ),
          ],
        ),
      ),
    );
  },
  child: const ListTile(
    leading: Icon(Icons.workspace_premium),
    title: Text('Premium'),
  ),
)

TooltipController exposes: onNext, onSkip, onDismiss, onPrevious, stepIndex, totalSteps, isLast, isFirst.


Controlling the Tour

final scope = IntroduceMeScope.of(context);

await scope.start('onboarding');
await scope.next();
await scope.previous();
await scope.skip();      // abandons the tour (does not mark completed)
await scope.dismiss();   // closes overlay / abandons
scope.setEnabled(false); // globally disable showcases

final active = scope.isActive;
final tourId = scope.activeTourId;
final rendered = scope.isTargetRendered();       // primary target
final byId = scope.isTargetRendered('fab-add');  // specific id

Start options

await scope.start(
  'onboarding',
  force: true,                 // ignore enableShowcase == false
  skipPersistenceCheck: true,  // show even if already completed
);

Typical first-run pattern

WidgetsBinding.instance.addPostFrameCallback((_) {
  IntroduceMeScope.of(context).start('onboarding');
});

Cross-screen tours

Tour steps can live on different routes (e.g. highlight a Settings icon on Home, then continue on a pushed Settings screen). Targets on unmounted routes are not in the registry until that route builds, so cross-screen needs a little setup.

Requirements

  1. Scope wraps the navigator — place IntroduceMeScopeWidget above the Navigator that pushes your screens so the overlay stays on top of pushed routes (OverlayPortal at root overlay).
  2. Attach the tour observer — register navigatorObserver on Navigator.observers so the tour resumes after push/pop.
  3. Use explicit order values — the engine advances by order, not widget-tree order. Keep gaps intentional (e.g. order 5 on Settings, order 7 on Home).
  4. Navigate from the prior step — use onTargetClick on the step that should open the next route (tap advances the tour and runs your navigation).
home: IntroduceMeScopeWidget(
  scopeName: 'main',
  routeSettleDelay: const Duration(milliseconds: 200),
  child: Builder(
    builder: (context) {
      final tourObserver = IntroduceMeScope.of(context).navigatorObserver;
      return Navigator(
        observers: [tourObserver],
        initialRoute: '/',
        onGenerateRoute: (settings) {
          switch (settings.name) {
            case '/settings':
              return MaterialPageRoute(
                settings: settings,
                builder: (_) => const SettingsScreen(), // Showcase.auto inside
              );
            default:
              return MaterialPageRoute(
                settings: settings,
                builder: (_) => const MainShell(), // previous steps
              );
          }
        },
      );
    },
  ),
),

On the step before the cross-screen target:

Showcase.auto(
  id: 'settings-action',
  tourId: 'onboarding',
  order: 4,
  title: 'Settings',
  description: 'Next step is on another route.',
  onTargetClick: () => Navigator.pushNamed(context, '/settings'),
  child: IconButton(
    icon: const Icon(Icons.settings_outlined),
    onPressed: () => Navigator.pushNamed(context, '/settings'),
  ),
),

On the pushed screen (not a descendant of the scope widget — registration still works via named scope):

Showcase.auto(
  id: 'settings-toggle',
  tourId: 'onboarding',
  order: 5,
  title: 'Cross-screen step',
  description: 'Registered when Settings mounts.',
  child: ListTile(title: Text('Cross-screen target')),
),

How resume works

Event Behavior
onTargetClick pushes a route Tour waits for Navigator.didPush, refreshes the step list, then advances to the next order
Order gap (e.g. 5 unmounted, 7 already on Home) Advance is deferred until the pushed route registers the missing step — the tour does not skip to 7
Target on a route under another Controller may request pop so the correct route is on top
TourStatus.waitingForTarget Shows scrollLoadingWidget (if set) while scrolling or waiting for a mount

Showcases on pushed routes use IntroduceMeScope.getNamed(scopeName) as a fallback when no IntroduceMeScopeInherited ancestor exists (see example/lib/screens/settings_screen.dart).

routeSettleDelay

After a route push/pop animation, the tour waits an extra beat before measuring target rects so tooltips do not appear offset mid-transition.

IntroduceMeScopeWidget(
  routeSettleDelay: const Duration(milliseconds: 200), // default: 120ms
  child: ...,
)
Symptom Try
Tooltip/highlight slightly misaligned right after navigation Increase routeSettleDelay (e.g. 200400 ms)
Feels sluggish after every route change Lower it (e.g. 80 ms) or Duration.zero

The delay runs after the route transition animation completes, not instead of it.

Replay & named scope

From a pushed screen without a scope ancestor:

await storage.resetTour('onboarding');
IntroduceMeScope.getNamed('main')?.start(
  'onboarding',
  skipPersistenceCheck: true,
  force: true,
);

See the example app — steps 4 (settings-action) and 5 (settings-toggle).


Tooltip Customization

Placement & arrow

Showcase.auto(
  id: 'side',
  tourId: 'onboarding',
  order: 3,
  title: 'Side tip',
  description: 'Tooltip can sit left or right of the target.',
  showArrow: true,
  tooltipPlacement: TooltipPlacement.left, // auto | above | below | left | right
  targetTooltipGap: 8,
  child: const Icon(Icons.info_outline),
)

TooltipPlacement.auto picks the side with the most free space.

Styling

Showcase.auto(
  id: 'styled',
  tourId: 'onboarding',
  order: 1,
  title: 'Styled',
  description: 'Colors, padding, and text styles.',
  textColor: Colors.white,
  tooltipBackgroundColor: const Color(0xFF1E1E2E),
  tooltipBorderRadius: BorderRadius.circular(16),
  tooltipPadding: const EdgeInsets.all(20),
  titleTextStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
  descTextStyle: const TextStyle(fontSize: 14, color: Colors.white70),
  titlePadding: const EdgeInsets.only(bottom: 4),
  descriptionPadding: const EdgeInsets.only(bottom: 8),
  child: const Text('Target'),
)

Action buttons

Default footer is Skip + Continue/OK. Override with tooltipActions:

Showcase.auto(
  id: 'actions',
  tourId: 'onboarding',
  order: 1,
  title: 'Custom actions',
  description: 'Configure footer buttons without Showcase.withWidget.',
  tooltipActionConfig: const TooltipActionConfig(
    alignment: MainAxisAlignment.end,
    axis: Axis.horizontal,
    gap: 8,
  ),
  tooltipActions: const [
    TooltipActionButton(label: 'Skip tour', type: TooltipActionType.skip),
    TooltipActionButton(
      label: 'Next',
      type: TooltipActionType.next,
      filled: true,
    ),
  ],
  child: const Icon(Icons.touch_app),
)
TooltipActionType Behavior
next Advance to next step
skip Abandon tour
dismiss Dismiss overlay
custom Only runs onTap
TooltipActionButton field Description
backgroundColor / textColor / textStyle Per-button styling (overrides look preset)
borderRadius / padding Button shape and insets
hideWhen bool Function(TooltipController) — hide button for current step

Action button position

By default buttons render inside the tooltip card. Use TooltipActionPosition.outside to place them below the card:

tooltipActionConfig: const TooltipActionConfig(
  position: TooltipActionPosition.outside,
  alignment: MainAxisAlignment.spaceBetween,
),
tooltipActions: [
  TooltipActionButton(
    label: 'Previous',
    type: TooltipActionType.custom,
    hideWhen: (c) => c.isFirst,
  ),
  const TooltipActionButton(
    label: 'Next',
    type: TooltipActionType.next,
    filled: true,
    backgroundColor: Color(0xFF7C4DFF),
  ),
],

Notes:

  • tooltipActions: null → default Skip/Continue
  • tooltipActions: [] → no footer buttons
  • Body tap still advances unless you rely only on buttons

Target highlight shape

Showcase.auto(
  id: 'shape',
  tourId: 'onboarding',
  order: 1,
  title: 'Shape',
  description: 'Circle or rounded cut-out.',
  targetShapeBorder: const CircleBorder(),
  // or: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))
  targetPadding: const EdgeInsets.all(6),
  targetBorderRadius: BorderRadius.circular(12), // used when shape is default
  overlayPadding: const EdgeInsets.all(2),       // also expands the hole
  child: const CircleAvatar(child: Icon(Icons.person)),
)

Effective hole inflate = overlayPadding + targetPadding.


Multi-Showcase

Highlight multiple widgets in a single step. Only the primary shows the tooltip.

Row(
  children: [
    Showcase.auto(
      id: 'chip-a',
      tourId: 'onboarding',
      order: 3,
      groupId: 'filters',
      isPrimary: true,
      title: 'Filters',
      description: 'Both chips are highlighted; tooltip anchors to primary.',
      tooltipPlacement: TooltipPlacement.right,
      child: const Chip(label: Text('A')),
    ),
    Showcase.auto(
      id: 'chip-b',
      tourId: 'onboarding',
      order: 3,
      groupId: 'filters',
      isPrimary: false,
      child: const Chip(label: Text('B')),
    ),
  ],
)

Rules:

  • Same tourId + order + groupId → one tour step
  • Soft cap: 8 targets per group
  • Secondary targets are holes only (no second tooltip)
  • groupId: null → normal single-target step

Overlay & Visual Effects

Barrier

IntroduceMeScopeWidget(
  barrierColor: const Color(0xB3000000),
  child: ...,
)

// Or per step / theme:
Showcase.auto(
  ...,
  overlayOpacity: 0.85,
  blurValue: 8, // 0 = off (default), capped at 16
)

Glass tooltip

Showcase.auto(
  ...,
  enableGlassEffect: true,
  glassBlurSigma: 16,
)

Floating action & scroll loading

Scope-level (applies to whole tour):

IntroduceMeScopeWidget(
  floatingActionWidget: TextButton(
    onPressed: () => IntroduceMeScope.of(context).skip(),
    child: const Text('Skip tour'),
  ),
  scrollLoadingWidget: const CircularProgressIndicator(),
  routeSettleDelay: const Duration(milliseconds: 200), // cross-screen layout settle
  child: ...,
)

Per-step overrides are also available via floatingActionWidget / scrollLoadingWidget on Showcase.

Animations

Defaults are performance-safe (fade/slide only; scale/moving off):

Showcase.auto(
  ...,
  disableAnimation: false,
  disableScaleAnimation: false,   // opt-in scale
  disableMovingAnimation: false,  // opt-in bounce-ish motion
  animationDuration: const Duration(milliseconds: 300),
  scaleAnimationDuration: const Duration(milliseconds: 300),
  toolTipSlideEndDistance: 8,
)

Auto-play

IntroduceMeScopeWidget(
  autoPlayDelay: const Duration(seconds: 3), // global
  child: ...,
)

Showcase.auto(
  ...,
  autoPlayDelay: const Duration(seconds: 2), // per-step override
)

Resilience & accessibility

IntroduceMeScopeWidget(
  enableSemantics: true,              // default — screen reader labels
  enableAutoScroll: true,             // default — scroll off-screen targets into view
  skipIfTargetNotPresent: false,      // when true, skip steps whose target never appears
  missingTargetTimeout: const Duration(seconds: 3),
  child: ...,
)

Showcase.auto(
  id: 'manual-scroll',
  tourId: 'onboarding',
  order: 1,
  title: 'Scroll me into view',
  enableAutoScroll: false,            // per-step opt-out
  scrollAlignment: 1.0,               // only applies when auto-scroll runs
  child: ...,
)
  • enableSemantics: false on a step disables Semantics on the hit area and default tooltip (useful only when you provide your own accessible UI).
  • With enableAutoScroll: false, the tour waits in waitingForTarget until the target is visible — scroll manually with your own ScrollController (see the example app's manual-scroll-item step).
  • skipIfTargetNotPresent pairs with TourAnalytics.onStepSkipped for funnel logging when a step is auto-skipped.

Gestures & Callbacks

Showcase.auto(
  id: 'gestures',
  tourId: 'onboarding',
  order: 1,
  title: 'Gestures',
  description: 'Wire product analytics or side effects.',
  onStart: () => debugPrint('step started'),
  onComplete: () => debugPrint('step completed'),
  onTargetClick: () => debugPrint('target tapped'),
  onTargetDoubleTap: () => debugPrint('double tap'),
  onTargetLongPress: () => debugPrint('long press'),
  onToolTipClick: () => debugPrint('tooltip body tapped'),
  onBarrierClick: () => debugPrint('barrier tapped'), // then dismisses
  onTargetRectUpdate: (rect) => debugPrint('rect=$rect'), // throttled
  disposeOnTap: false,
  disableDefaultTargetGestures: false,
  disableBarrierInteraction: false,
  child: const Icon(Icons.ads_click),
)
Callback When
onStart / onComplete Step activate / deactivate
onTargetClick Target tap (also advances unless disposeOnTap). If the next step by order is on another route, navigation runs first and the tour waits for that route to mount before advancing.
onTargetDoubleTap / onTargetLongPress Extra gestures (no auto-advance)
onToolTipClick Tooltip body tap (advances)
onBarrierClick Barrier tap, then tour dismisses
onTargetRectUpdate Target rect changed (throttled ~16ms)
Scope onFinish Tour completed successfully
Scope onDismiss Tour ended early — TourDismissReason.skipped, .barrierTap, .disabledScope, or .programmatic (not fired on normal completion)
IntroduceMeScopeWidget(
  onDismiss: (tourId, reason) => debugPrint('Dismissed $tourId: $reason'),
  child: ...,
)

Branching Tours (onDetermineNextStep)

By default the tour advances in order (by order). Set onDetermineNextStep on IntroduceMeScopeWidget (or TourController / IntroduceMeScopeHandle) to run your own logic every time the tour is about to move forward — decide, per app state, whether it should jump to a specific step or just continue normally.

The callback is invoked before every automatic advance (next(), tooltip/target tap, auto-play, and auto-skip of a missing target). It receives:

  • currentStep — the ShowcaseRegistration being left (null right before the very first step).
  • currentIndex — its index within steps (-1 for the first step).
  • steps — the full, current list of resolved ShowcaseRegistrations for the active tour.

Return the index (within steps) the tour should jump to next, or null to run normally (next step by order). Returning an index that is negative or >= steps.length ends the tour, same as running out of steps.

IntroduceMeScopeWidget(
  onDetermineNextStep: (currentStep, currentIndex, steps) {
    // Example: skip the "manage account" step for guests, going straight
    // to "sign up" instead. Any other step runs normally.
    if (currentStep?.config.id == 'step_profile' && !userIsLoggedIn) {
      final target = steps.indexWhere((s) => s.config.id == 'step_sign_up');
      return target >= 0 ? target : null;
    }
    return null; // normal, in-order flow
  },
  child: ...,
)

It can also be set/overridden at runtime through the scope handle:

final scope = IntroduceMeScope.of(context);
scope.onDetermineNextStep = (currentStep, currentIndex, steps) {
  if (currentStep?.config.id == 'step_has_pets' && !userHasPets) {
    return steps.indexWhere((s) => s.config.id == 'step_wrap_up');
  }
  return null;
};

Notes:

  • The callback may be async (returns FutureOr<int?>) — safe to read app/user state from a repository or database before deciding.
  • It runs for every advance, not just once — return null whenever the default flow should apply so unrelated steps are unaffected.
  • Works alongside cross-screen tours: steps always reflects the freshly resolved registry for the active tour, so a jump target that just mounted on a pushed route is included.
  • See example/lib for a runnable "branching tour" demo.

Persistence

IntroduceMeScopeWidget(
  storage: SharedPrefsTourStorage(), // production
  // storage: MemoryTourStorage(),   // tests / demos
  child: ...,
)

Behavior:

  • start(tourId) no-ops if the tour was already marked completed
  • Successful finish → markTourCompleted(tourId)
  • skip() / abandon → not marked completed
  • Force replay: start('onboarding', skipPersistenceCheck: true)

Custom storage:

class MyTourStorage implements TourStorage {
  @override
  Future<bool> isTourCompleted(String tourId) async => ...;

  @override
  Future<void> markTourCompleted(String tourId) async => ...;

  @override
  Future<void> resetTour(String tourId) async => ...;
}

Analytics

IntroduceMeScopeWidget(
  analytics: TourAnalytics(
    onStepShown: (tourId, stepId, duration) { /* log */ },
    onStepSkipped: (tourId, stepId) { /* log */ },
    onStepCompleted: (tourId, stepId, duration) { /* log */ },
    onTourAbandoned: (tourId, done, total) { /* log */ },
    onTourFinished: (tourId, duration) { /* log */ },
  ),
  child: ...,
)

Use this for onboarding funnels (drop-off per step, completion rate, time-to-finish).


Theming

Global defaults via ThemeExtension:

ThemeData(
  useMaterial3: true,
  extensions: const [
    IntroduceMeTheme(
      barrierColor: Color(0xB3000000),
      overlayOpacity: 1.0,
      blurValue: 0,
      tooltipBackgroundColor: Color(0xFF1E1E2E),
      textColor: Colors.white,
      tooltipBorderRadius: BorderRadius.all(Radius.circular(12)),
      tooltipPadding: EdgeInsets.all(16),
      toolTipMargin: EdgeInsets.all(14),
      targetTooltipGap: 12,
      targetPadding: EdgeInsets.zero,
      showArrow: true,
      enableGlassEffect: false,
      glassBlurSigma: 12,
    ),
  ],
)

Per-step props override theme values when set.

Look presets

Set a global default via IntroduceMeTheme(look: ShowcaseLook.retro) or per step with Showcase.auto(look: ShowcaseLook.ios). A look styles the tooltip card, action buttons, and target highlight ring — never barrierColor, overlayOpacity, or blurValue.

Precedence: explicit field → look preset → IntroduceMeTheme field → built-in default.

ShowcaseLook Character
material M3 ColorScheme surface + elevation shadow
ios Cupertino frosted panel, thin highlight ring
glass Glassmorphism (enableGlassEffect)
neumorphism Soft dual-shadow panel, blended buttons
retro Flat fill, hard offset shadow, thick border
neoBrutalism Square corners, boldest border/shadow/ring
ThemeData(
  extensions: const [
    IntroduceMeTheme(look: ShowcaseLook.glass, enableGlassEffect: true),
  ],
)

Showcase.auto(
  look: ShowcaseLook.neoBrutalism, // overrides global look for this step
  ...,
)

Showcase.withWidget still accepts look for the target highlight ring only; custom tooltip UIs are unaffected.


Named Scopes

Useful when you need access without BuildContext, or multiple independent tours.

IntroduceMeScopeWidget(
  scopeName: 'main',
  child: ...,
)

// Anywhere later:
IntroduceMeScope.getNamed('main')?.start('onboarding');
IntroduceMeScope.getNamed('main')?.isTargetRendered('fab-add');

Still prefer IntroduceMeScope.of(context) inside the widget tree when possible.


Tour Builder

Optional fluent API for declarative definitions (especially with GlobalKeys):

final menuKey = GlobalKey();
final profileKey = GlobalKey();

await tour('onboarding')
    .step(key: menuKey, title: 'Menu', description: 'Open the drawer')
    .step(key: profileKey, title: 'Profile', description: 'Your account')
    .persist(storage: SharedPrefsTourStorage())
    .start(context);

For most apps, Showcase.auto + start(tourId) is simpler.


Performance Tips

  1. Prefer Showcase.auto + id over large GlobalKey lists
  2. Keep costly features off unless needed: blurValue: 0, scale/moving animations disabled (defaults)
  3. Use disableAnimation: true on low-end devices if needed
  4. Keep multi-showcase groups small (soft cap 8); only one primary tooltip is rendered
  5. Scope tours per feature — avoid hundreds of steps in one tour
  6. Use MemoryTourStorage in tests, SharedPrefsTourStorage in production
  7. Position updates are throttled (~16ms) and coalesced to one overlay rebuild per frame
  8. Overlay mutations are deferred off the build phase to avoid jank

API Reference

Widgets & entry points

API Description
IntroduceMeScopeWidget Root scope (registry + controller + overlay)
IntroduceMeScope of, maybeOf, getNamed
IntroduceMeScopeHandle start, next, previous, skip, dismiss, setEnabled, isTargetRendered, navigatorObserver, onDetermineNextStep
Showcase Manual step wrapper
Showcase.auto Auto-discovery step
Showcase.withWidget Custom tooltip step
IntroduceMeExtension.introduceMe / .showcase One-line wrap
ShowCaseWidget / ShowcaseScope Aliases of IntroduceMeScopeWidget

Tour engine

API Description
TourController Runtime tour state machine
TourStatus idle, running, waitingForTarget, completed
TourDismissReason skipped, barrierTap, disabledScope, programmatic
OnDetermineNextStep FutureOr<int?> Function(ShowcaseRegistration? currentStep, int currentIndex, List<ShowcaseRegistration> steps) — branching hook, see Branching Tours
routeSettleDelay Extra pause after route transitions before cross-screen layout measure (on IntroduceMeScopeWidget, default 120ms)
TourRegistry ID / tour / group registration index
TourBuilder / tour() Fluent declarative builder
ShowcaseStepConfig Immutable step configuration (enableSemantics, enableAutoScroll, skipIfTargetNotPresent, look, …)
TooltipPlacement auto, above, below, left, right

Tooltip actions

API Description
TooltipActionButton Footer button model (backgroundColor, hideWhen, …)
TooltipActionConfig Alignment / axis / gap / position
TooltipActionType next, skip, dismiss, custom
TooltipActionPosition inside (default), outside
TooltipController Controller passed to custom tooltips

Persistence & analytics

API Description
TourStorage Persistence interface
SharedPrefsTourStorage SharedPreferences implementation
MemoryTourStorage In-memory (tests/dev)
TourAnalytics Funnel callback hooks

Theming & rendering

API Description
IntroduceMeTheme Global defaults (ThemeExtension, optional look)
ShowcaseLook / ShowcaseLookStyle Named visual presets + resolved styling
OverlayHost Single reusable overlay portal
TargetLayer Barrier + cut-out highlight(s)
TooltipLayer Tooltip + arrow + actions
PositionTracker Throttled target rect tracking

Example App

cd example
flutter run

The example is a full onboarding tour with 22 ordered steps (tourId: 'onboarding') that demonstrates every major feature. See the demo at the top of this README for a full walkthrough.

Tour step map

Order ID Screen Features demonstrated
0 arrow-demo Home showArrow, TooltipPlacement.below, tooltipActions, TooltipActionConfig, targetPadding
1 nav-home Shell Showcase.auto, bottom nav target
2 fab-add Shell enableGlassEffect, TooltipPlacement.above, CircleBorder, overlayPadding
3 nav-profile Shell Tab sync via IndexedStack
4 settings-action AppBar onTargetClick → push Settings, cross-screen hand-off
5 settings-toggle Settings Cross-screen target (registers on push), named scope replay
7 multi-a / multi-b Home Multi-showcase (groupId, isPrimary), TooltipPlacement.above
8 side-tooltip Home TooltipPlacement.left, opt-in scale animation
9 styled-tooltip Home textColor, paddings, alignment, targetBorderRadius
10 blur-overlay Home blurValue, overlayOpacity, disableBarrierInteraction
11 dismiss-demo Home disposeOnTap — tap target dismisses tour
12 gestures-demo Home onStart/onComplete, tap/double-tap/long-press, onToolTipClick, onBarrierClick, onTargetRectUpdate
13 animations-demo Home disableScaleAnimation / disableMovingAnimation off, autoPlayDelay
14 featured-item Home .introduceMe(auto: true), lazy list auto-scroll
15 avatar Profile Manual Showcase, targetShapeBorder, onTargetClick
16 custom-tooltip Profile Showcase.withWidget, TooltipController.onPrevious
19 semantics-off Home enableSemantics: false comparison step
20 styled-actions Home Action backgroundColor/textStyle, hideWhen, TooltipActionPosition.outside
21 look-override Home Per-step look: ShowcaseLook.neoBrutalism
22 manual-scroll-item Home enableAutoScroll: false + manual ScrollController

Scope-level features (main.dart)

Feature Where
IntroduceMeScopeWidget + scopeName: 'main' main.dart
Nested Navigator + navigatorObserver main.dart (cross-screen)
routeSettleDelay main.dart (200ms — tune if tooltip shifts after navigation)
MemoryTourStorage (shared exampleTourStorage) example_storage.dart
TourAnalytics (all hooks) main.dart
onFinish main.dart
onDismiss + SnackBar main.dart (TourDismissReason)
floatingActionWidget Skip tour button overlay
scrollLoadingWidget Spinner while scrolling to off-screen target
IntroduceMeTheme light / dark + look dropdown main.dart + Settings
Manual scroll (enableAutoScroll: false) manual-scroll-item + ScrollController
skipIfTargetNotPresent demo Settings resilience-demo tour
setEnabled(false) demo Settings scope toggle
IntroduceMeScope.getNamed('main') Settings replay & dismiss
TourBuilder / tour() Settings builder demo (builder-demo tour)
SharedPrefsTourStorage Documented in Settings (example uses MemoryTourStorage)
Persistence replay resetTour + start(force, skipPersistenceCheck)

File layout

example/lib/
├── main.dart              # Scope, analytics, shell, FAB, nav
├── example_storage.dart   # Shared MemoryTourStorage
├── example_app_state.dart # Theme mode + global look notifiers
└── screens/
    ├── home_screen.dart   # Tooltip, multi, blur, gestures, scroll
    ├── profile_screen.dart# Manual Showcase + withWidget
    └── settings_screen.dart # Cross-screen + isTargetRendered + TourBuilder

Try it

  1. Run the app — tour starts automatically after first frame.
  2. Step through with Next or use Skip tour (floating action).
  3. At step 4, tap the Settings icon — tour pushes Settings and continues at step 5 (see Cross-screen tours).
  4. On Settings, tap Replay onboarding tour to restart.
  5. In Settings, switch theme mode to compare IntroduceMeTheme (glass on FAB in dark mode).
  6. In Settings, tap Start builder demo tour for TourBuilder / tour().

Migration Notes

Coming from a GlobalKey-based showcase package?

Old pattern Introduce Me
Collect keys, startShowCase([k1, k2]) Tag with Showcase.auto, start('tourId')
Manual “already seen” flag SharedPrefsTourStorage
Custom footer buttons via full custom tooltip tooltipActions
Multiple highlights groupId + isPrimary
Context-only controller Named scope: IntroduceMeScope.getNamed('main')
Cross-screen steps navigatorObserver + onTargetClick + consistent order; see Cross-screen tours

Alias available: ShowCaseWidgetIntroduceMeScopeWidget.


License

MIT — see LICENSE.

Built with ❤️ for Flutter developers who deserve better onboarding.

Libraries

introduceme
Introduce Me — flexible, lightweight Flutter showcase and onboarding package.