Perché il ciclo di vita conta
Ogni app mobile attraversa continuamente diversi stati: viene messa in background quando l'utente riceve una telefonata, torna in foreground quando riprende l'attività, oppure viene chiusa del tutto. Reagire correttamente a questi eventi è fondamentale per salvare lo stato dell'utente, mettere in pausa animazioni o video, liberare risorse e gestire le connessioni di rete.
Storicamente in Flutter si usava WidgetsBindingObserver con il metodo didChangeAppLifecycleState. A partire da Flutter 3.13 è disponibile un'API dedicata e più espressiva: AppLifecycleListener, che offre callback granulari per ogni transizione di stato.
Gli stati del ciclo di vita
L'enum AppLifecycleState prevede i seguenti valori:
- resumed: l'app è visibile e riceve input dall'utente.
- inactive: l'app è visibile ma non riceve input (es. durante una chiamata in arrivo o lo split-screen).
- hidden: l'app non è più visibile (stato introdotto per uniformare il comportamento tra piattaforme).
- paused: l'app è in background e non visibile.
- detached: l'app è ancora ospitata dall'engine ma senza view collegate; tipico durante l'avvio o la chiusura.
Le transizioni seguono un percorso ben definito. Passando in background l'ordine è: resumed → inactive → hidden → paused. Al ritorno in foreground il percorso è inverso.
L'approccio classico con WidgetsBindingObserver
Prima di vedere la nuova API, ecco il metodo tradizionale, ancora perfettamente valido:
class _HomePageState extends State<HomePage>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
// Salva lo stato dell'utente
}
}
@override
Widget build(BuildContext context) => const Scaffold();
}
Questo approccio funziona, ma costringe a un unico metodo didChangeAppLifecycleState in cui gestire tutti i casi con una serie di if.
La nuova API: AppLifecycleListener
AppLifecycleListener espone callback separate per ogni transizione, rendendo il codice più leggibile:
import 'package:flutter/widgets.dart';
class LifecycleManager {
late final AppLifecycleListener _listener;
void init() {
_listener = AppLifecycleListener(
onResume: () => debugPrint('App tornata in primo piano'),
onInactive: () => debugPrint('App inattiva'),
onHide: () => debugPrint('App nascosta'),
onPause: () => debugPrint('App in background'),
onDetach: () => debugPrint('App scollegata'),
onShow: () => debugPrint('App di nuovo visibile'),
onStateChange: (state) => debugPrint('Nuovo stato: $state'),
);
}
void dispose() {
_listener.dispose();
}
}
È importante chiamare sempre dispose() sul listener per evitare memory leak.
Intercettare la richiesta di chiusura
Una delle funzionalità più interessanti è onExitRequested, che permette di intercettare la richiesta di chiusura dell'app (principalmente su desktop) e decidere se consentirla o annullarla, ad esempio per mostrare un dialog di conferma:
_listener = AppLifecycleListener(
onExitRequested: () async {
final shouldExit = await _mostraDialogConferma();
return shouldExit
? AppExitResponse.exit
: AppExitResponse.cancel;
},
);
Questo è utilissimo per app che devono salvare lavoro non completato prima di chiudersi.
Esempio pratico in un widget
Ecco come integrare AppLifecycleListener in uno StatefulWidget per mettere in pausa un video quando l'app va in background:
class VideoScreen extends StatefulWidget {
const VideoScreen({super.key});
@override
State<VideoScreen> createState() => _VideoScreenState();
}
class _VideoScreenState extends State<VideoScreen> {
late final AppLifecycleListener _listener;
@override
void initState() {
super.initState();
_listener = AppLifecycleListener(
onPause: _pausaVideo,
onResume: _riprendiVideo,
);
}
void _pausaVideo() {
// controller.pause();
}
void _riprendiVideo() {
// controller.play();
}
@override
void dispose() {
_listener.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => const Scaffold();
}
Differenze tra piattaforme
È bene ricordare alcune particolarità:
- Su iOS lo stato
detachedpuò non essere sempre emesso alla chiusura, perché il sistema può terminare il processo bruscamente. onExitRequestedè rilevante soprattutto su desktop (Windows, macOS, Linux) e sul web; su mobile la chiusura è gestita diversamente dal sistema operativo.- Lo stato
hiddenè stato aggiunto per uniformare il comportamento: prima non tutte le piattaforme lo emettevano.
Best practice
- Salva sempre lo stato critico in
onPause(opaused), non affidarti adetached, che non è garantito. - Rilascia le risorse pesanti (camera, microfono, connessioni) quando l'app passa in background per rispettare i limiti del sistema.
- Non eseguire operazioni lunghe nelle callback: lo stato può cambiare rapidamente.
- Usa
onStateChangesolo quando ti serve una logica trasversale a più stati, altrimenti preferisci le callback specifiche.
Conclusione
AppLifecycleListener rappresenta un'evoluzione naturale rispetto a WidgetsBindingObserver: offre un'API più chiara, callback dedicate e la possibilità di intercettare la chiusura dell'app. Integrarlo nelle proprie app significa gestire in modo affidabile il salvataggio dei dati e le risorse, migliorando l'esperienza utente su tutte le piattaforme.
