6 Modi per Gestire lo Stato nelle App Flutter (Con Esempi di Codice)

Come gestire lo stato in Flutter

Indice

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:

  1. 🧱 setState() — L’approccio più semplice incorporato
  2. 🏗️ InheritedWidget — La base di Flutter per la propagazione dello stato
  3. 🪄 Provider — La soluzione consigliata per la maggior parte delle app
  4. 🔒 Riverpod — Evoluzione moderna e sicura a livello di compilazione di Provider
  5. 📦 Bloc — Per applicazioni scalabili e a livello enterprise
  6. GetX — Soluzione leggera e completa

flutter troubles mega tracktor


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.