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
- 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: ...,
)
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 |
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 |
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.