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.
