Perché l'onboarding conta

Le prime interazioni di un utente con la tua app determinano spesso se la userà davvero o la disinstallerà entro pochi minuti. Un buon flusso di onboarding presenta le funzionalità chiave, spiega i permessi necessari e riduce l'abbandono iniziale. In questa guida vedremo come costruire un onboarding completo in Flutter usando introduction_screen per le schermate introduttive e shared_preferences per ricordare se l'utente ha già completato il tour.

Le dipendenze

Aggiungi i pacchetti al tuo pubspec.yaml:

dependencies:
  introduction_screen: ^3.1.14
  shared_preferences: ^2.2.3

Poi esegui flutter pub get.

Rilevare la prima esecuzione

Il primo passo è capire se mostrare o meno l'onboarding. Salviamo un flag persistente con shared_preferences.

import 'package:shared_preferences/shared_preferences.dart';

class OnboardingService {
  static const _key = 'onboarding_completed';

  Future<bool> isCompleted() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(_key) ?? false;
  }

  Future<void> setCompleted() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_key, true);
  }
}

All'avvio dell'app decidiamo quale schermata mostrare:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final completed = await OnboardingService().isCompleted();
  runApp(MyApp(showOnboarding: !completed));
}

class MyApp extends StatelessWidget {
  final bool showOnboarding;
  const MyApp({super.key, required this.showOnboarding});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: showOnboarding ? const OnboardingPage() : const HomePage(),
    );
  }
}

Costruire le schermate introduttive

Il widget IntroductionScreen gestisce automaticamente indicatori di pagina, pulsanti di navigazione e skip. Ogni pagina è definita da un PageViewModel.

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

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

  @override
  Widget build(BuildContext context) {
    final pageDecoration = PageDecoration(
      titleTextStyle: Theme.of(context).textTheme.headlineSmall!,
      bodyTextStyle: Theme.of(context).textTheme.bodyLarge!,
      imagePadding: const EdgeInsets.all(24),
    );

    return IntroductionScreen(
      globalBackgroundColor: Colors.white,
      pages: [
        PageViewModel(
          title: 'Benvenuto',
          body: 'Organizza le tue attività quotidiane in modo semplice.',
          image: const Icon(Icons.task_alt, size: 120),
          decoration: pageDecoration,
        ),
        PageViewModel(
          title: 'Sincronizzazione',
          body: 'I tuoi dati sono sempre al sicuro e sincronizzati sul cloud.',
          image: const Icon(Icons.cloud_sync, size: 120),
          decoration: pageDecoration,
        ),
        PageViewModel(
          title: 'Iniziamo',
          body: 'Sei pronto a essere piu produttivo? Cominciamo!',
          image: const Icon(Icons.rocket_launch, size: 120),
          decoration: pageDecoration,
        ),
      ],
      onDone: () => _completeOnboarding(context),
      onSkip: () => _completeOnboarding(context),
      showSkipButton: true,
      skip: const Text('Salta'),
      next: const Icon(Icons.arrow_forward),
      done: const Text('Fine', style: TextStyle(fontWeight: FontWeight.w600)),
      dotsDecorator: DotsDecorator(
        activeColor: Theme.of(context).primaryColor,
        size: const Size(10, 10),
        activeSize: const Size(22, 10),
        activeShape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(25),
        ),
      ),
    );
  }

  Future<void> _completeOnboarding(BuildContext context) async {
    await OnboardingService().setCompleted();
    if (context.mounted) {
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(builder: (_) => const HomePage()),
      );
    }
  }
}

Nota l'uso di context.mounted dopo l'operazione asincrona: è una best practice per evitare di usare un BuildContext non più valido.

Feature discovery: guidare dentro l'app

L'onboarding non finisce con le schermate iniziali. Spesso serve evidenziare singole funzionalità direttamente nell'interfaccia. Un pattern comune è mostrare dei tooltip contestuali solo la prima volta.

class HighlightTip extends StatefulWidget {
  final Widget child;
  final String message;
  final String prefKey;

  const HighlightTip({
    super.key,
    required this.child,
    required this.message,
    required this.prefKey,
  });

  @override
  State<HighlightTip> createState() => _HighlightTipState();
}

class _HighlightTipState extends State<HighlightTip> {
  final _tooltipKey = GlobalKey<TooltipState>();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      final prefs = await SharedPreferences.getInstance();
      final shown = prefs.getBool(widget.prefKey) ?? false;
      if (!shown && mounted) {
        _tooltipKey.currentState?.ensureTooltipVisible();
        await prefs.setBool(widget.prefKey, true);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Tooltip(
      key: _tooltipKey,
      message: widget.message,
      triggerMode: TooltipTriggerMode.manual,
      child: widget.child,
    );
  }
}

Con questo widget puoi avvolgere qualsiasi elemento e mostrare un suggerimento contestuale solo alla prima apparizione.

Best practice per un onboarding efficace

  • Sii breve: tre o quattro schermate al massimo. Troppe pagine aumentano l'abbandono.
  • Consenti sempre di saltare: gli utenti esperti vogliono arrivare subito al contenuto.
  • Mostra valore, non funzionalità: comunica i benefici, non l'elenco tecnico delle feature.
  • Rispetta i temi: usa Theme.of(context) invece di colori hardcoded per supportare dark mode.
  • Testa il reset: durante lo sviluppo aggiungi un'opzione nascosta per resettare il flag e rivedere l'onboarding.

Conclusione

Con introduction_screen e shared_preferences hai tutti gli strumenti per costruire un onboarding professionale in poche righe di codice. Ricorda che l'onboarding è la prima impressione: investi tempo nel renderlo chiaro, accessibile e coerente con l'identità visiva della tua app. Un buon primo contatto si traduce direttamente in una maggiore retention degli utenti.