introduceme 1.0.1 copy "introduceme: ^1.0.1" to clipboard
introduceme: ^1.0.1 copied to clipboard

Introduce Me — a flexible, lightweight Flutter showcase and onboarding package with a smart tour engine and lean rendering core.

example/lib/main.dart

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

import 'branding.dart';
import 'example_app_state.dart';
import 'example_storage.dart';
import 'screens/home_screen.dart';
import 'screens/profile_screen.dart';
import 'screens/settings_screen.dart';

void main() {
  runApp(const IntroduceMeExampleApp());
}

/// Root navigator key so [onDismiss] can show a SnackBar without a local context.
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

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

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<ThemeMode>(
      valueListenable: exampleThemeMode,
      builder: (context, themeMode, _) {
        return ValueListenableBuilder<ShowcaseLook?>(
          valueListenable: exampleLook,
          builder: (context, selectedLook, child) {
            return MaterialApp(
              title: 'Introduce Me Example',
              debugShowCheckedModeBanner: false,
              themeMode: themeMode,
              theme: ThemeData(
                colorScheme: ColorScheme.fromSeed(
                  seedColor: AnonimeactBranding.primaryCyan,
                  brightness: Brightness.light,
                ),
                useMaterial3: true,
                extensions: [
                  IntroduceMeTheme(
                    look: selectedLook,
                    barrierColor: const Color(0xB3000000),
                    tooltipBackgroundColor: const Color(0xFF1E1E2E),
                    enableGlassEffect: false,
                    blurValue: 0,
                  ),
                ],
              ),
              darkTheme: ThemeData(
                colorScheme: ColorScheme.fromSeed(
                  seedColor: AnonimeactBranding.primaryCyan,
                  brightness: Brightness.dark,
                ),
                useMaterial3: true,
                extensions: [
                  IntroduceMeTheme(
                    look: selectedLook,
                    barrierColor: const Color(0xCC000000),
                    tooltipBackgroundColor: const Color(0xFF2A2A3E),
                    enableGlassEffect: true,
                    glassBlurSigma: 16,
                  ),
                ],
              ),
              // Scope on `home` with a nested Navigator so pushed routes (e.g.
              // Settings) stay under the same overlay host subtree.
              home: IntroduceMeScopeWidget(
                scopeName: 'main',
                storage: exampleTourStorage,
                routeSettleDelay: const Duration(milliseconds: 500),
                onDismiss: (tourId, reason) {
                  debugPrint('👋 Dismissed $tourId: $reason');
                  final messenger = ScaffoldMessenger.maybeOf(
                    navigatorKey.currentContext ?? context,
                  );
                  messenger?.showSnackBar(
                    SnackBar(
                      content: Text('Tour dismissed: ${reason.name}'),
                      duration: const Duration(seconds: 2),
                    ),
                  );
                },
                scrollLoadingWidget: Builder(
                  builder: (context) {
                    final theme = Theme.of(context);
                    return Material(
                      color: theme.colorScheme.surface.withValues(alpha: 0.92),
                      borderRadius: BorderRadius.circular(16),
                      elevation: 4,
                      child: Padding(
                        padding: const EdgeInsets.all(24),
                        child: Column(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            const CircularProgressIndicator(),
                            const SizedBox(height: 12),
                            Text('Loading…', style: theme.textTheme.bodyMedium),
                          ],
                        ),
                      ),
                    );
                  },
                ),
                floatingActionWidget: Builder(
                  builder: (context) {
                    return Material(
                      color: Theme.of(
                        context,
                      ).colorScheme.surface.withValues(alpha: 0.9),
                      borderRadius: BorderRadius.circular(24),
                      elevation: 4,
                      child: TextButton.icon(
                        onPressed: () => IntroduceMeScope.of(context).skip(),
                        icon: const Icon(Icons.skip_next),
                        label: const Text('Skip tour'),
                      ),
                    );
                  },
                ),
                analytics: TourAnalytics(
                  onStepShown: (tourId, stepId, _) {
                    debugPrint('📍 Step shown: $tourId/$stepId');
                    if (stepId == 'manual-scroll-item') {
                      MainShell.scrollToManualScrollItem?.call();
                    }
                  },
                  onStepSkipped: (tourId, stepId) {
                    debugPrint('⏭️ Step skipped: $tourId/$stepId');
                  },
                  onStepCompleted: (tourId, stepId, duration) {
                    debugPrint(
                      '✓ Step done: $tourId/$stepId (${duration.inMilliseconds}ms)',
                    );
                  },
                  onTourAbandoned: (tourId, done, total) {
                    debugPrint('⚠️ Abandoned: $tourId ($done/$total)');
                  },
                  onTourFinished: (tourId, duration) {
                    debugPrint(
                      '✅ Tour finished: $tourId in ${duration.inMilliseconds}ms',
                    );
                  },
                ),
                onFinish: () => debugPrint('🎉 Onboarding complete!'),
                // Branches `branching-demo` (see Settings > "Branching demo")
                // by `exampleUserIsMember`; every other tour just runs
                // normally, in order.
                onDetermineNextStep: (currentStep, currentIndex, steps) {
                  if (currentStep?.config.tourId != 'branching-demo') {
                    return null;
                  }

                  switch (currentStep?.config.id) {
                    case 'branch-start':
                      final target = exampleUserIsMember.value
                          ? 'branch-member'
                          : 'branch-guest';
                      final idx = steps.indexWhere(
                        (s) => s.config.id == target,
                      );
                      return idx >= 0 ? idx : null;
                    case 'branch-guest':
                      // Guests skip straight past the Member step.
                      final idx = steps.indexWhere(
                        (s) => s.config.id == 'branch-final',
                      );
                      return idx >= 0 ? idx : null;
                    default:
                      return null; // e.g. branch-member -> branch-final normally
                  }
                },
                child: Builder(
                  builder: (context) {
                    final tourObserver = IntroduceMeScope.of(
                      context,
                    ).navigatorObserver;
                    return Navigator(
                      key: navigatorKey,
                      observers: [tourObserver],
                      initialRoute: '/',
                      onGenerateRoute: (settings) {
                        switch (settings.name) {
                          case '/settings':
                            return MaterialPageRoute<void>(
                              settings: settings,
                              builder: (_) => const SettingsScreen(),
                            );
                          case '/':
                          default:
                            return MaterialPageRoute<void>(
                              settings: settings,
                              builder: (_) => const MainShell(),
                            );
                        }
                      },
                    );
                  },
                ),
              ),
            );
          },
        );
      },
    );
  }
}

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

  /// Called from analytics when the manual-scroll step becomes active.
  static VoidCallback? scrollToManualScrollItem;

  @override
  State<MainShell> createState() => _MainShellState();
}

class _MainShellState extends State<MainShell> {
  static const _profileStepIds = {'avatar', 'custom-tooltip', 'nav-profile'};
  static const _shellHomeStepIds = {'nav-home', 'fab-add', 'settings-action'};
  static const _homeStepIds = {
    'arrow-demo',
    'featured-item',
    'manual-scroll-item',
    'multi-a',
    'side-tooltip',
    'styled-tooltip',
    'styled-actions',
    'look-override',
    'semantics-off',
    'blur-overlay',
    'dismiss-demo',
    'gestures-demo',
    'animations-demo',
  };

  int _tabIndex = 0;
  TourController? _tourController;
  final ScrollController _homeScrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    MainShell.scrollToManualScrollItem = _scrollToManualItem;
    prepareOnboardingReplay = _prepareForOnboardingReplay;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final scope = IntroduceMeScope.of(context);
      _tourController = scope.controller;
      _tourController!.addListener(_syncTabWithTour);
      scope.start('onboarding');
    });
  }

  void _scrollToManualItem() {
    if (!_homeScrollController.hasClients) return;
    _homeScrollController.animateTo(
      _homeScrollController.position.maxScrollExtent * 0.55,
      duration: const Duration(milliseconds: 450),
      curve: Curves.easeInOut,
    );
  }

  void _prepareForOnboardingReplay() {
    if (!mounted) return;
    if (_tabIndex != 0) {
      setState(() => _tabIndex = 0);
    }
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_homeScrollController.hasClients) {
        _homeScrollController.jumpTo(0);
      }
    });
  }

  @override
  void dispose() {
    MainShell.scrollToManualScrollItem = null;
    prepareOnboardingReplay = null;
    _tourController?.removeListener(_syncTabWithTour);
    _homeScrollController.dispose();
    super.dispose();
  }

  void _syncTabWithTour() {
    if (!mounted) return;
    final stepId = _tourController?.activeConfig?.id;
    if (stepId == null) return;

    if (_profileStepIds.contains(stepId) && _tabIndex != 1) {
      setState(() => _tabIndex = 1);
    } else if ((_homeStepIds.contains(stepId) ||
            _shellHomeStepIds.contains(stepId)) &&
        _tabIndex != 0) {
      setState(() => _tabIndex = 0);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: AnonimeactBranding.logoSmallImage(height: 24),
        centerTitle: false,
        actions: [
          Showcase.auto(
            id: 'settings-action',
            tourId: 'onboarding',
            order: 4,
            title: 'Settings',
            description:
                'Opens Settings — next step is on another route (cross-screen).',
            showArrow: true,
            tooltipPlacement: TooltipPlacement.below,
            onTargetClick: () {
              Navigator.pushNamed(context, '/settings');
            },
            child: IconButton(
              icon: const Icon(Icons.settings_outlined),
              onPressed: () => Navigator.pushNamed(context, '/settings'),
            ),
          ),
        ],
      ),
      body: IndexedStack(
        index: _tabIndex,
        children: [
          HomeScreen(scrollController: _homeScrollController),
          const ProfileScreen(),
        ],
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _tabIndex,
        onDestinationSelected: (i) => setState(() => _tabIndex = i),
        destinations: [
          NavigationDestination(
            icon: Showcase.auto(
              id: 'nav-home',
              tourId: 'onboarding',
              order: 1,
              title: 'Home',
              description: 'Auto-discovery on bottom navigation.',
              showArrow: true,
              child: const Icon(Icons.home_outlined),
            ),
            label: 'Home',
          ),
          NavigationDestination(
            icon: Showcase.auto(
              id: 'nav-profile',
              tourId: 'onboarding',
              order: 3,
              title: 'Profile',
              description: 'Tab sync switches IndexedStack during the tour.',
              showArrow: true,
              child: const Icon(Icons.person_outline),
            ),
            label: 'Profile',
          ),
        ],
      ),
      floatingActionButton: Showcase.auto(
        id: 'fab-add',
        tourId: 'onboarding',
        order: 2,
        title: 'Quick Add',
        description:
            'Glass tooltip (enableGlassEffect) + TooltipPlacement.above.',
        targetShapeBorder: const CircleBorder(),
        enableGlassEffect: true,
        showArrow: true,
        tooltipPlacement: TooltipPlacement.above,
        targetTooltipGap: 8,
        overlayPadding: const EdgeInsets.all(2),
        child: FloatingActionButton(
          onPressed: () {
            ScaffoldMessenger.of(
              context,
            ).showSnackBar(const SnackBar(content: Text('FAB tapped!')));
          },
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
4
likes
160
points
149
downloads

Documentation

API reference

Publisher

verified publisheranonimeact.com

Weekly Downloads

Introduce Me — a flexible, lightweight Flutter showcase and onboarding package with a smart tour engine and lean rendering core.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, shared_preferences

More

Packages that depend on introduceme