Introduce Me
A flexible, lightweight Flutter showcase & onboarding package.
Smart tour engine, auto-discovery, and lean rendering — built for real apps.
Table of Contents
- Why Introduce Me?
- Installation
- Quick Start
- Feature Overview
- Core Concepts
- Tagging Targets
- Controlling the Tour
- Cross-screen tours
- Tooltip Customization
- Multi-Showcase
- Overlay & Visual Effects
- Gestures & Callbacks
- Branching Tours (
onDetermineNextStep) - Persistence
- Analytics
- Theming
- Named Scopes
- Tour Builder
- Performance Tips
- API Reference
- Example App
- Migration Notes
- License
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
Showcase.auto (recommended)
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
- Scope wraps the navigator — place
IntroduceMeScopeWidgetabove theNavigatorthat pushes your screens so the overlay stays on top of pushed routes (OverlayPortalat root overlay). - Attach the tour observer — register
navigatorObserveronNavigator.observersso the tour resumes after push/pop. - Use explicit
ordervalues — the engine advances byorder, not widget-tree order. Keep gaps intentional (e.g. order5on Settings, order7on Home). - Navigate from the prior step — use
onTargetClickon 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. 200–400 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/ContinuetooltipActions: []→ 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: falseon a step disablesSemanticson the hit area and default tooltip (useful only when you provide your own accessible UI).- With
enableAutoScroll: false, the tour waits inwaitingForTargetuntil the target is visible — scroll manually with your ownScrollController(see the example app'smanual-scroll-itemstep). skipIfTargetNotPresentpairs withTourAnalytics.onStepSkippedfor 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— theShowcaseRegistrationbeing left (nullright before the very first step).currentIndex— its index withinsteps(-1for the first step).steps— the full, current list of resolvedShowcaseRegistrations 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(returnsFutureOr<int?>) — safe to read app/user state from a repository or database before deciding. - It runs for every advance, not just once — return
nullwhenever the default flow should apply so unrelated steps are unaffected. - Works alongside cross-screen tours:
stepsalways reflects the freshly resolved registry for the active tour, so a jump target that just mounted on a pushed route is included. - See
example/libfor 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
- Prefer
Showcase.auto+idover largeGlobalKeylists - Keep costly features off unless needed:
blurValue: 0, scale/moving animations disabled (defaults) - Use
disableAnimation: trueon low-end devices if needed - Keep multi-showcase groups small (soft cap 8); only one primary tooltip is rendered
- Scope tours per feature — avoid hundreds of steps in one tour
- Use
MemoryTourStoragein tests,SharedPrefsTourStoragein production - Position updates are throttled (~16ms) and coalesced to one overlay rebuild per frame
- 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
- Run the app — tour starts automatically after first frame.
- Step through with Next or use Skip tour (floating action).
- At step 4, tap the Settings icon — tour pushes Settings and continues at step 5 (see Cross-screen tours).
- On Settings, tap Replay onboarding tour to restart.
- In Settings, switch theme mode to compare
IntroduceMeTheme(glass on FAB in dark mode). - 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: ShowCaseWidget → IntroduceMeScopeWidget.
License
MIT — see LICENSE.
Built with ❤️ for Flutter developers who deserve better onboarding.
Libraries
- introduceme
- Introduce Me — flexible, lightweight Flutter showcase and onboarding package.