Lezione 8 – Form e Validazione in Flutter: struttura professionale, controller esterni e SharedPreferences

I form in Flutter sembrano banali.

Finché non devono gestire:

  • login reale
  • errori backend
  • loading asincrono
  • persistenza del token
  • UX coerente
  • separazione tra UI e business logic

La Lezione 8 del corso entra nel dettaglio di come costruire form professionali, scalabili e integrati in un’architettura MVVM con Riverpod.

📺 Guarda la Lezione 8 su YouTube
https://youtu.be/dh0SBic9cWw

📁 Slide ufficiali della lezione
https://drive.google.com/file/d/1xnRVRo8vOVtF_AQXiyM02WcgjFiEOtb8/view?usp=sharing


Il problema dei Form “da tutorial”

Molti esempi online mostrano qualcosa del genere:

TextFormField(
  controller: TextEditingController(),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return "Campo obbligatorio";
    }
    return null;
  },
)

Sembra corretto.

Ma in un’app reale questo approccio genera:

  • controller creati dentro il widget
  • validazione hardcoded
  • logica mischiata alla UI
  • difficoltà di testing
  • codice non riutilizzabile

Il problema non è il TextFormField.

Il problema è la struttura.


Architettura corretta di un Form in Flutter

Nel contesto del corso utilizziamo:

  • MVVM
  • Riverpod
  • ChangeNotifier
  • Controller esterni
  • Validatori separati

Il principio è semplice:

La UI mostra.
Il ViewModel gestisce stato e logica.


Controller esterni al widget

Nel tuo approccio ogni campo:

  • riceve il controller dall’esterno
  • non crea istanze inline
  • non contiene logica di business

ViewModel

class LoginViewModel extends ChangeNotifier {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  bool isLoading = false;
  String? errorMessage;

  String? validateEmail(String? value) {
    if (value == null || value.isEmpty) {
      return "Email obbligatoria";
    }
    if (!value.contains("@")) {
      return "Email non valida";
    }
    return null;
  }
}

UI

TextFormField(
  controller: viewModel.emailController,
  validator: viewModel.validateEmail,
)

Separazione chiara.
Responsabilità chiare.


Validazione scalabile

La validazione deve essere:

  • riutilizzabile
  • isolata
  • testabile

Esempio con classe dedicata:

class Validators {
  static String? required(String? value) {
    if (value == null || value.isEmpty) {
      return "Campo obbligatorio";
    }
    return null;
  }

  static String? email(String? value) {
    if (value == null || !value.contains("@")) {
      return "Email non valida";
    }
    return null;
  }
}

Questo permette:

  • riuso su più form
  • codice leggibile
  • zero duplicazioni

Integrazione con Riverpod

Il ViewModel viene esposto tramite provider:

final loginViewModelProvider =
    ChangeNotifierProvider((ref) => LoginViewModel());

Nel widget:

final viewModel = ref.watch(loginViewModelProvider);

Vantaggi:

  • rebuild controllati
  • lifecycle gestito
  • stato centralizzato

Nessun setState sparso.


Gestione di Loading, Error e Success

Un form professionale deve gestire:

  • loading
  • errore backend
  • stato finale

Nel ViewModel:

Future<void> login() async {
  isLoading = true;
  notifyListeners();

  try {
    await authService.login(
      emailController.text,
      passwordController.text,
    );
  } catch (e) {
    errorMessage = "Credenziali non valide";
  }

  isLoading = false;
  notifyListeners();
}

Nella UI:

ElevatedButton(
  onPressed: viewModel.isLoading ? null : viewModel.login,
  child: viewModel.isLoading
      ? CircularProgressIndicator()
      : Text("Login"),
)

La UI reagisce.
Non decide.


SharedPreferences come Singleton

Questa lezione introduce anche un concetto chiave per la Lezione 9:
persistenza locale tramite SharedPreferences inizializzate una sola volta.

Implementazione

class LocalStorage {
  static late SharedPreferences _prefs;

  static Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
  }

  static void setToken(String token) {
    _prefs.setString("token", token);
  }

  static String? getToken() {
    return _prefs.getString("token");
  }
}

Nel main():

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await LocalStorage.init();
  runApp(MyApp());
}

Questo evita:

  • async sparsi nel codice
  • inizializzazioni multiple
  • dipendenze disordinate

Ed è la base per la gestione token e interceptor nella Lezione 9.


Errori comuni nei Form Flutter

  • controller creati dentro il build
  • validazione duplicata
  • logica API dentro il widget
  • stato loading gestito in UI
  • SharedPreferences usate con await in ogni schermata

Conclusione

Un form non è solo una schermata.

È:

  • stato
  • validazione
  • UX
  • persistenza
  • preparazione all’autenticazione

La Lezione 8 prepara il terreno per:

  • gestione token
  • clean architecture
  • app Flutter in produzione

📺 Lezione 8 su YouTube
https://youtu.be/dh0SBic9cWw

📁 Slide ufficiali
https://drive.google.com/file/d/1xnRVRo8vOVtF_AQXiyM02WcgjFiEOtb8/view?usp=sharing

📚 Playlist completa del corso Flutter gratuito
https://www.youtube.com/watch?v=Y0aZvwv3puk&list=PL5DSRxOKWSGLGAtbSLWoe_EsEz29zHSag

Condividi il post:

Leggi anche