6 Modi per Gestire lo Stato nelle App Flutter (Con Esempi di Codice)
Come gestire lo stato in Flutter
In questo articolo, esploreremo sei modi popolari per gestire lo stato in Flutter app, inclusi esempi reali e best practice:
Gestione dello stato è uno dei temi più importanti - e dibattuti - nello sviluppo Flutter. Determina come l’app gestisce i cambiamenti dei dati e aggiorna l’interfaccia utente in modo efficiente. Flutter ti offre diverse strategie per gestire lo stato — da semplici a altamente scalabili.
Le strategie per la gestione dello stato in Flutter sono:
- 🧱
setState()
— L’approccio più semplice incorporato - 🏗️ InheritedWidget — La base di Flutter per la propagazione dello stato
- 🪄 Provider — La soluzione consigliata per la maggior parte delle app
- 🔒 Riverpod — Evoluzione moderna e sicura a livello di compilazione di Provider
- 📦 Bloc — Per applicazioni scalabili e a livello enterprise
- ⚡ GetX — Soluzione leggera e completa
1. Utilizzo di setState()
— I fondamenti
Il modo più semplice per gestire lo stato in Flutter è utilizzando il metodo incorporato setState()
in un StatefulWidget
.
Questo approccio è ideale per lo stato dell’interfaccia utente locale — dove lo stato appartiene a un widget e non deve essere condiviso nell’app.
Esempio: App Contatore con setState()
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: CounterScreen());
}
}
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('setState Counter')),
body: Center(
child: Text('Count: $_count', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: Icon(Icons.add),
),
);
}
}
Vantaggi
- Molto semplice da implementare
- Ideale per lo stato locale o temporaneo dell’interfaccia utente
- Nessuna dipendenza esterna
Svantaggi
- Non scalabile per app molto grandi
- Difficile condividere lo stato tra i widget
- Logica mescolata con l’interfaccia utente
Utilizzalo quando:
- Prototipando o costruendo piccoli widget
- Gestendo uno stato dell’interfaccia utente isolato (es. attivare un pulsante, mostrare un modal)
2. InheritedWidget — La base di Flutter
InheritedWidget
è il meccanismo di basso livello che Flutter utilizza per propagare i dati nell’albero dei widget. La maggior parte delle soluzioni di gestione dello stato (incluso Provider) sono costruite su di esso.
Comprendere InheritedWidget ti aiuta a capire come funziona la gestione dello stato di Flutter sotto il cofano.
Esempio: Gestore del tema con InheritedWidget
import 'package:flutter/material.dart';
// L'InheritedWidget che tiene i dati del tema
class AppTheme extends InheritedWidget {
final bool isDarkMode;
final Function toggleTheme;
const AppTheme({
Key? key,
required this.isDarkMode,
required this.toggleTheme,
required Widget child,
}) : super(key: key, child: child);
// Metodo di accesso per ottenere l'AppTheme più vicino nell'albero dei widget
static AppTheme? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppTheme>();
}
@override
bool updateShouldNotify(AppTheme oldWidget) {
return isDarkMode != oldWidget.isDarkMode;
}
}
// Wrapper stato per gestire i cambiamenti
class ThemeManager extends StatefulWidget {
final Widget child;
const ThemeManager({Key? key, required this.child}) : super(key: key);
@override
_ThemeManagerState createState() => _ThemeManagerState();
}
class _ThemeManagerState extends State<ThemeManager> {
bool _isDarkMode = false;
void _toggleTheme() {
setState(() {
_isDarkMode = !_isDarkMode;
});
}
@override
Widget build(BuildContext context) {
return AppTheme(
isDarkMode: _isDarkMode,
toggleTheme: _toggleTheme,
child: widget.child,
);
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ThemeManager(
child: Builder(
builder: (context) {
final theme = AppTheme.of(context);
return MaterialApp(
theme: theme!.isDarkMode ? ThemeData.dark() : ThemeData.light(),
home: HomeScreen(),
);
},
),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context);
return Scaffold(
appBar: AppBar(title: Text('InheritedWidget Theme')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Current Theme: ${theme!.isDarkMode ? "Dark" : "Light"}',
style: TextStyle(fontSize: 20),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => theme.toggleTheme(),
child: Text('Toggle Theme'),
),
],
),
),
);
}
}
Vantaggi
- Incorporato in Flutter — nessuna dipendenza esterna
- Aggiornamenti efficienti dei widget
- Base per comprendere altre soluzioni
- Controllo diretto sulla logica di propagazione
Svantaggi
- Codice boilerplate verboso
- Richiede un wrapper StatefulWidget
- Facile commettere errori
- Non adatto ai principianti
Utilizzalo quando:
- Imparando come funziona la gestione dello stato di Flutter internamente
- Costruendo soluzioni personalizzate di gestione dello stato
- Hai bisogno di un controllo molto specifico sulla propagazione dello stato
3. Provider — La soluzione consigliata da Flutter
Quando lo stato dell’app deve essere condiviso tra diversi widget, Provider ti soccorre.
Provider si basa su Inversione del controllo — invece che i widget che possiedono lo stato, un provider lo espone per essere consumato da altri. Il team Flutter raccomanda ufficialmente Provider per le app di dimensioni medie.
Configurazione
Aggiungi questo al tuo pubspec.yaml
:
dependencies:
provider: ^6.0.5
Esempio: App Contatore con Provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
),
);
}
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: CounterScreen());
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Scaffold(
appBar: AppBar(title: Text('Provider Counter')),
body: Center(
child: Text('Count: ${counter.count}', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: context.read<CounterModel>().increment,
child: Icon(Icons.add),
),
);
}
}
Vantaggi
- Aggiornamenti reattivi ed efficienti
- Separazione pulita tra interfaccia utente e logica
- Ben documentato e supportato dalla comunità
Svantaggi
- Slightly più boilerplate rispetto a
setState()
- I provider annidati possono diventare complessi
Utilizzalo quando:
- Hai bisogno di uno stato condiviso tra diversi widget
- Vuoi un modello reattivo senza complessità
- L’app sta crescendo oltre la fase di prototipo
4. Riverpod — L’evoluzione moderna di Provider
Riverpod è un completo riscrittura di Provider che elimina la sua dipendenza da BuildContext e aggiunge la sicurezza a livello di compilazione. È progettato per risolvere i limiti di Provider mantenendo la stessa filosofia.
Configurazione
Aggiungi questo al tuo pubspec.yaml
:
dependencies:
flutter_riverpod: ^2.4.0
Esempio: Profilo Utente con Riverpod
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Model
class UserProfile {
final String name;
final int age;
UserProfile({required this.name, required this.age});
UserProfile copyWith({String? name, int? age}) {
return UserProfile(
name: name ?? this.name,
age: age ?? this.age,
);
}
}
// State Notifier
class ProfileNotifier extends StateNotifier<UserProfile> {
ProfileNotifier() : super(UserProfile(name: 'Guest', age: 0));
void updateName(String name) {
state = state.copyWith(name: name);
}
void updateAge(int age) {
state = state.copyWith(age: age);
}
}
// Provider
final profileProvider = StateNotifierProvider<ProfileNotifier, UserProfile>((ref) {
return ProfileNotifier();
});
// Provider calcolato
final greetingProvider = Provider<String>((ref) {
final profile = ref.watch(profileProvider);
return 'Hello, ${profile.name}! You are ${profile.age} years old.';
});
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: ProfileScreen());
}
}
class ProfileScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final profile = ref.watch(profileProvider);
final greeting = ref.watch(greetingProvider);
return Scaffold(
appBar: AppBar(title: Text('Riverpod Profile')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(greeting, style: TextStyle(fontSize: 20)),
SizedBox(height: 30),
TextField(
decoration: InputDecoration(labelText: 'Name'),
onChanged: (value) {
ref.read(profileProvider.notifier).updateName(value);
},
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Age: ${profile.age}', style: TextStyle(fontSize: 18)),
SizedBox(width: 20),
ElevatedButton(
onPressed: () {
ref.read(profileProvider.notifier).updateAge(profile.age + 1);
},
child: Text('+'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
if (profile.age > 0) {
ref.read(profileProvider.notifier).updateAge(profile.age - 1);
}
},
child: Text('-'),
),
],
),
],
),
),
);
}
}
Vantaggi
- Nessuna dipendenza da BuildContext — accesso allo stato ovunque
- Sicurezza a livello di compilazione — cattura errori prima dell’esecuzione
- Maggiore testabilità
- Potente composizione dello stato
- Nessun memory leak
Svantaggi
- API diversa da quelle tradizionali di Flutter
- Curva di apprendimento più ripida rispetto a Provider
- Comunità più piccola (ma in crescita rapidamente)
Utilizzalo quando:
- Iniziando un nuovo progetto Flutter
- Vuoi la sicurezza tipologica e le garanzie a livello di compilazione
- Dipendenze complesse e composizioni dello stato
- Lavorando su app di dimensioni medie a grandi
5. BLoC (Business Logic Component) — Per App Enterprise
BLoC pattern è un’architettura più avanzata che separa completamente la logica aziendale dall’interfaccia utente utilizzando stream.
È ideale per app di grandi dimensioni o enterprise, dove le transizioni di stato prevedibili e testabili sono essenziali.
Configurazione
Aggiungi questa dipendenza al tuo progetto:
dependencies:
flutter_bloc: ^9.0.0
Esempio: App Contatore con BLoC
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Eventi
abstract class CounterEvent {}
class Increment extends CounterEvent {}
// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: MaterialApp(home: CounterScreen()),
);
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bloc Counter')),
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Text('Count: $count', style: TextStyle(fontSize: 24));
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: Icon(Icons.add),
),
);
}
}
Vantaggi
- Scalabile e mantenibile per app complesse
- Separazione chiara dei livelli
- Facile da testare
Svantaggi
- Più boilerplate
- Curva di apprendimento più ripida
Utilizzalo quando:
- Costruendo progetti enterprise o a lungo termine
- Hai bisogno di logica prevedibile e testabile
- Più sviluppatori lavorano su moduli diversi dell’app
6. GetX — La soluzione leggera completa
GetX è una soluzione minimalista ma potente per la gestione dello stato che include anche il routing, l’iniezione delle dipendenze e le utilità. È noto per avere il meno boilerplate tra tutte le soluzioni.
Configurazione
Aggiungi questo al tuo pubspec.yaml
:
dependencies:
get: ^4.6.5
Esempio: Elenco di acquisti con GetX
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// Controller
class ShoppingController extends GetxController {
// Lista osservabile
var items = <String>[].obs;
// Contatore osservabile
var totalItems = 0.obs;
void addItem(String item) {
if (item.isNotEmpty) {
items.add(item);
totalItems.value = items.length;
}
}
void removeItem(int index) {
items.removeAt(index);
totalItems.value = items.length;
}
void clearAll() {
items.clear();
totalItems.value = 0;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'GetX Shopping List',
home: ShoppingScreen(),
);
}
}
class ShoppingScreen extends StatelessWidget {
// Inizializza controller
final ShoppingController controller = Get.put(ShoppingController());
final TextEditingController textController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Shopping List'),
actions: [
Obx(() => Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
'Items: ${controller.totalItems}',
style: TextStyle(fontSize: 18),
),
),
)),
],
),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: textController,
decoration: InputDecoration(
hintText: 'Enter item name',
border: OutlineInputBorder(),
),
),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
controller.addItem(textController.text);
textController.clear();
},
child: Icon(Icons.add),
),
],
),
),
Expanded(
child: Obx(() {
if (controller.items.isEmpty) {
return Center(
child: Text('No items in your list'),
);
}
return ListView.builder(
itemCount: controller.items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(controller.items[index]),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () => controller.removeItem(index),
),
);
},
);
}),
),
if (controller.items.isNotEmpty)
Padding(
padding: EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
Get.defaultDialog(
title: 'Clear List',
middleText: 'Are you sure you want to clear all items?',
textConfirm: 'Yes',
textCancel: 'No',
onConfirm: () {
controller.clearAll();
Get.back();
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
minimumSize: Size(double.infinity, 50),
),
child: Text('Clear All'),
),
),
],
),
);
}
}
Vantaggi
- Minimo boilerplate — sviluppo più rapido
- Soluzione completa (stato, routing, DI, snackbars, dialoghi)
- Nessuna necessità di BuildContext
- Molto leggero e veloce
- Facile da imparare
Svantaggi
- Meno “Flutter-like” rispetto a Provider/BLoC
- Può portare a un’alta dipendenza se non si è attenti
- Ecosistema più piccolo rispetto a Provider
- Alcuni sviluppatori lo trovano “troppo magico”
Utilizzalo quando:
- Prototipazione rapida o sviluppo MVP
- App piccole a medie
- Hai bisogno di un minimo boilerplate
- Il team preferisce la semplicità rispetto all’architettura rigorosa
Scegliere la soluzione giusta per la gestione dello stato
Soluzione | Complessità | Boilerplate | Curva di apprendimento | Migliore per |
---|---|---|---|---|
🧱 setState() |
Basso | Minimo | Facile | Stato locale semplice, prototipi |
🏗️ InheritedWidget |
Medio | Alto | Medio | Imparare gli interni di Flutter, soluzioni personalizzate |
🪄 Provider |
Basso-Medio | Basso | Facile | La maggior parte delle app, stato condiviso |
🔒 Riverpod |
Medio | Basso-Medio | Medio | App moderne, sicurezza tipologica |
📦 BLoC |
Alto | Alto | Ripida | App enterprise, logica aziendale complessa |
⚡ GetX |
Basso | Minimo | Facile | Sviluppo rapido, MVP |
Guida rapida per la decisione:
Complessità dell’app | Approccio consigliato | Caso d’uso esempio |
---|---|---|
🪶 Semplice | setState() o GetX |
Attivare pulsanti, animazioni, piccoli widget |
⚖️ Medio | Provider o Riverpod |
Temi condivisi, impostazioni utente, caching dati |
🏗️ Complesso | BLoC o Riverpod |
E-commerce, app chat, dashboard finanziari |
Pensieri finali
Non esiste un approccio adatto a tutti per la gestione dello stato in Flutter.
Ecco un approccio pratico:
- Inizia semplice con
setState()
per widget e prototipi semplici - Impara i fondamenti con InheritedWidget per comprendere come funziona internamente Flutter
- Passa a Provider quando l’app cresce e ha bisogno di stato condiviso
- Considera Riverpod per nuovi progetti dove la sicurezza tipologica e i modelli moderni contano
- Adotta BLoC per applicazioni enterprise con logica aziendale complessa
- Prova GetX quando la velocità di sviluppo e il minimo boilerplate sono prioritari
Punti chiave:
✅ Non sovraingegnerare — utilizza setState()
finché non ne hai bisogno
✅ Provider e Riverpod sono eccellenti scelte per la maggior parte delle applicazioni
✅ BLoC eccelle in grandi team e app complesse
✅ GetX è perfetto per la velocità, ma fai attenzione alla dipendenza
✅ Comprendere InheritedWidget ti aiuta a padroneggiare qualsiasi soluzione
La chiave è bilanciare semplicità, scalabilità e manutenibilità — e scegliere lo strumento giusto per le tue specifiche esigenze, competenze del team e requisiti del progetto.
Link alle librerie di gestione dello stato di Flutter
- Documentazione ufficiale di Flutter sulla gestione dello stato
- Pacchetto Provider
- Documentazione di Riverpod
- Libreria BLoC
- Documentazione di GetX