Perché un buon sistema di temi è fondamentale

Un'applicazione professionale deve garantire coerenza visiva, supporto al tema scuro e facilità di manutenzione. Flutter offre un sistema di theming potente, soprattutto dopo l'introduzione di Material 3 (Material You), che ridefinisce il modo in cui colori, tipografia e componenti vengono gestiti.

In questo articolo vedremo come configurare Material 3, generare uno schema di colori coerente, estendere il tema con valori personalizzati tramite ThemeExtension e implementare un cambio di tema dinamico.

Abilitare Material 3

Dalle versioni recenti di Flutter, Material 3 è attivo per impostazione predefinita. Possiamo comunque configurarlo esplicitamente partendo da un colore seme (seedColor), dal quale Flutter genera l'intera palette armonica.

import 'package:flutter/material.dart';

final ThemeData lightTheme = ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(
    seedColor: const Color(0xFF6750A4),
    brightness: Brightness.light,
  ),
);

final ThemeData darkTheme = ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(
    seedColor: const Color(0xFF6750A4),
    brightness: Brightness.dark,
  ),
);

Usando lo stesso seedColor per entrambe le luminosità otteniamo coerenza cromatica tra tema chiaro e scuro.

Applicare i temi a MaterialApp

L'oggetto MaterialApp accetta theme, darkTheme e themeMode. Quest'ultimo determina quale tema usare: system segue le impostazioni del dispositivo, light e dark forzano una modalità specifica.

MaterialApp(
  theme: lightTheme,
  darkTheme: darkTheme,
  themeMode: ThemeMode.system,
  home: const HomePage(),
);

Personalizzare la tipografia e i componenti

Oltre ai colori, possiamo definire stili di testo e personalizzare i singoli widget tramite i rispettivi Theme data.

ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
  textTheme: const TextTheme(
    titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
    bodyMedium: TextStyle(fontSize: 16, height: 1.4),
  ),
  filledButtonTheme: FilledButtonThemeData(
    style: FilledButton.styleFrom(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
      ),
    ),
  ),
);

Estendere il tema con ThemeExtension

Il ColorScheme di Material 3 copre molti casi, ma spesso un'app ha bisogno di colori semantici aggiuntivi (es. il verde "successo", il giallo "avviso") non previsti dallo schema standard. La soluzione idiomatica è ThemeExtension.

import 'package:flutter/material.dart';

class AppColors extends ThemeExtension<AppColors> {
  final Color success;
  final Color warning;

  const AppColors({required this.success, required this.warning});

  @override
  AppColors copyWith({Color? success, Color? warning}) {
    return AppColors(
      success: success ?? this.success,
      warning: warning ?? this.warning,
    );
  }

  @override
  AppColors lerp(ThemeExtension<AppColors>? other, double t) {
    if (other is! AppColors) return this;
    return AppColors(
      success: Color.lerp(success, other.success, t)!,
      warning: Color.lerp(warning, other.warning, t)!,
    );
  }
}

Il metodo lerp garantisce transizioni animate fluide quando si cambia tema. Registriamo l'estensione nel ThemeData:

final lightTheme = ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
  extensions: const [
    AppColors(success: Color(0xFF2E7D32), warning: Color(0xFFF9A825)),
  ],
);

E la recuperiamo nei widget tramite il Theme:

Widget build(BuildContext context) {
  final appColors = Theme.of(context).extension<AppColors>()!;
  return Container(color: appColors.success);
}

Cambio di tema dinamico

Per permettere all'utente di scegliere il tema, possiamo gestire lo stato del themeMode. Ecco un esempio semplice con ValueNotifier, facilmente sostituibile con il gestore di stato che preferite.

class ThemeController {
  static final themeMode = ValueNotifier<ThemeMode>(ThemeMode.system);

  static void toggle() {
    themeMode.value = themeMode.value == ThemeMode.dark
        ? ThemeMode.light
        : ThemeMode.dark;
  }
}
ValueListenableBuilder<ThemeMode>(
  valueListenable: ThemeController.themeMode,
  builder: (context, mode, _) {
    return MaterialApp(
      theme: lightTheme,
      darkTheme: darkTheme,
      themeMode: mode,
      home: const HomePage(),
    );
  },
);

Per rendere la scelta persistente tra una sessione e l'altra, basta salvare il valore con shared_preferences e ricaricarlo all'avvio.

Best practice

  • Usa il ColorScheme: evita colori hardcoded nei widget, accedi sempre a Theme.of(context).colorScheme per garantire il corretto adattamento alla dark mode.
  • Centralizza il tema: definisci i temi in un unico file, così da mantenere coerenza e facilitare modifiche future.
  • Sfrutta ThemeExtension: per ogni valore di design ricorrente non coperto dallo schema standard.
  • Testa entrambe le modalità: controlla sempre il contrasto e la leggibilità sia in chiaro che in scuro.

Conclusione

Material 3 rende il theming in Flutter più espressivo e coerente. Combinando lo schema generato dal seedColor, le personalizzazioni dei componenti e la flessibilità di ThemeExtension, è possibile costruire un design system scalabile e pronto per la dark mode dinamica. Un investimento iniziale che ripaga in manutenibilità e qualità dell'esperienza utente.