[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-edge-to-edge-in-flutter-gestire-notch-barre-di-sistema-e-android-15":3,"comments-article-edge-to-edge-in-flutter-gestire-notch-barre-di-sistema-e-android-15":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},100,"Edge-to-edge in Flutter: gestire notch, barre di sistema e Android 15","edge-to-edge-in-flutter-gestire-notch-barre-di-sistema-e-android-15","Da Android 15 l'edge-to-edge è obbligatorio e Flutter lo abilita di default. Guida pratica a SafeArea, MediaQuery padding e viewPadding, SystemUiOverlayStyle e AnnotatedRegion per UI che si estendono sotto le barre di sistema senza contenuti tagliati.","## Perché l'edge-to-edge è diventato un tema urgente\n\nCon Android 15 (API 35) Google ha reso l'**edge-to-edge obbligatorio**: le app che targettizzano quel livello di API disegnano automaticamente sotto la status bar e sotto la barra di navigazione, e le vecchie API per colorare quelle barre (`Window.setStatusBarColor`, `setNavigationBarColor`) sono deprecate e ignorate. Dal canto suo, Flutter ha allineato il framework abilitando l'edge-to-edge di default sulle versioni recenti di Android a partire da Flutter 3.27.\n\nIn pratica: se non intervieni, potresti ritrovarti con un pulsante coperto dalla barra gestuale, un titolo sotto l'orologio della status bar, o icone bianche su sfondo bianco. Su iOS il problema esiste da anni (notch, Dynamic Island, home indicator), ma il modello concettuale di Flutter è lo stesso. Vediamo come governarlo in modo pulito.\n\n## Il modello: padding, viewPadding e viewInsets\n\nTutto ruota attorno a tre proprietà di `MediaQuery`, spesso confuse tra loro:\n\n- **`viewInsets`**: spazio occupato da qualcosa che *copre completamente* la UI e con cui non si può interagire, tipicamente la **tastiera software**.\n- **`viewPadding`**: spazio occupato dalle intrusioni fisiche o di sistema (notch, status bar, barra di navigazione, home indicator). È **stabile**, non cambia quando compare la tastiera.\n- **`padding`**: è `viewPadding` meno la parte già \"consumata\" da `viewInsets`. Quando la tastiera si apre, `padding.bottom` scende a zero perché la home indicator è coperta dalla tastiera.\n\nRegola pratica:\n\n- usa **`padding`** (o `SafeArea`) per posizionare contenuti che devono adattarsi dinamicamente;\n- usa **`viewPadding`** quando ti serve una misura stabile, ad esempio l'altezza di una barra inferiore custom che non deve \"saltare\" all'apertura della tastiera;\n- usa **`viewInsets.bottom`** per lasciare spazio alla tastiera in un foglio modale.\n\nIn Flutter 3.10+ esistono i metodi granulari che evitano rebuild inutili: preferisci `MediaQuery.paddingOf(context)` a `MediaQuery.of(context).padding`, perché il widget si ricostruisce solo quando cambia quella specifica proprietà.\n\n```dart\nfinal padding = MediaQuery.paddingOf(context);      \u002F\u002F dinamico\nfinal viewPadding = MediaQuery.viewPaddingOf(context); \u002F\u002F stabile\nfinal keyboard = MediaQuery.viewInsetsOf(context).bottom;\n```\n\n## Abilitare (o forzare) l'edge-to-edge\n\nSu Android 15 è il comportamento di default, ma se vuoi coerenza anche su versioni precedenti puoi impostarlo esplicitamente all'avvio:\n\n```dart\nimport 'package:flutter\u002Fservices.dart';\n\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n  SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);\n  runApp(const MyApp());\n}\n```\n\n`SystemUiMode.edgeToEdge` mantiene visibili le barre di sistema ma rende l'app responsabile dell'intera superficie dello schermo. Evita `SystemUiMode.manual` con overlay rimossi se non stai costruendo un player video o un'esperienza immersiva: nasconde le barre e crea problemi di accessibilità.\n\n## Controllare le icone delle barre di sistema\n\nCon l'edge-to-edge non puoi più colorare la barra: puoi solo decidere se le sue **icone** devono essere chiare o scure. Si fa con `SystemUiOverlayStyle`, idealmente per schermata tramite `AnnotatedRegion`, così ogni pagina dichiara lo stile corretto e Flutter lo ripristina automaticamente al pop della route.\n\n```dart\nclass DarkHeaderPage extends StatelessWidget {\n  const DarkHeaderPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return AnnotatedRegion\u003CSystemUiOverlayStyle>(\n      value: const SystemUiOverlayStyle(\n        statusBarColor: Colors.transparent,\n        \u002F\u002F Android: icone chiare su header scuro\n        statusBarIconBrightness: Brightness.light,\n        \u002F\u002F iOS: attenzione, la logica è invertita\n        statusBarBrightness: Brightness.dark,\n        systemNavigationBarColor: Colors.transparent,\n        systemNavigationBarIconBrightness: Brightness.light,\n        systemNavigationBarContrastEnforced: false,\n      ),\n      child: const Scaffold(body: _Content()),\n    );\n  }\n}\n```\n\nDue note importanti:\n\n- su iOS `statusBarBrightness` indica la **luminosità dello sfondo**, quindi va impostato all'opposto rispetto ad Android;\n- `systemNavigationBarContrastEnforced: false` disattiva lo scrim semitrasparente che Android applica dietro la barra di navigazione. Toglilo solo se il tuo contenuto garantisce già contrasto sufficiente, altrimenti la barra gestuale diventa illeggibile.\n\nSe usi una `AppBar`, Material 3 imposta già uno stile coerente con il tema; `AppBar.systemOverlayStyle` permette di sovrascriverlo puntualmente.\n\n## SafeArea: usarla bene, non ovunque\n\n`SafeArea` è il modo più semplice per non finire sotto le barre, ma applicata a tutto il body produce l'errore classico: **liste che si tagliano** invece di scorrere sotto la status bar.\n\nIl pattern corretto per una lista a tutto schermo è disabilitare il lato interessato e spostare l'inset nel `padding` dello scroll view:\n\n```dart\nScaffold(\n  body: SafeArea(\n    bottom: false, \u002F\u002F lasciamo scorrere il contenuto sotto la barra gestuale\n    child: ListView.builder(\n      padding: EdgeInsets.only(\n        bottom: MediaQuery.paddingOf(context).bottom + 16,\n      ),\n      itemCount: items.length,\n      itemBuilder: (context, index) => ListTile(title: Text(items[index])),\n    ),\n  ),\n)\n```\n\nCosì lo sfondo della lista arriva fino al bordo dello schermo, ma l'ultimo elemento resta raggiungibile.\n\nUn'alternativa elegante per gli scroll view è `MediaQuery.removePadding`, utile quando un widget figlio applicherebbe un inset già gestito dal genitore.\n\n## Barre inferiori custom e `extendBody`\n\nI widget Material come `NavigationBar`, `BottomNavigationBar` e `BottomAppBar` gestiscono già gli inset inferiori. Se invece costruisci una barra personalizzata, devi aggiungere tu il padding e — importante — usare `viewPadding` per evitare che la barra si restringa quando si apre la tastiera:\n\n```dart\nclass GlassBottomBar extends StatelessWidget {\n  const GlassBottomBar({super.key, required this.child});\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    final bottomInset = MediaQuery.viewPaddingOf(context).bottom;\n    return Container(\n      padding: EdgeInsets.fromLTRB(16, 12, 16, 12 + bottomInset),\n      color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.9),\n      child: child,\n    );\n  }\n}\n```\n\nSe vuoi che il contenuto scorra **sotto** una barra traslucida con effetto blur, imposta `extendBody: true` sullo `Scaffold` (e `extendBodyBehindAppBar: true` per la parte superiore):\n\n```dart\nScaffold(\n  extendBody: true,\n  extendBodyBehindAppBar: true,\n  bottomNavigationBar: const GlassBottomBar(child: _Tabs()),\n  body: const _Content(),\n)\n```\n\n## Tastiera e bottom sheet\n\nUn bottom sheet con campi di testo deve sollevarsi con la tastiera. Il modo più diretto è combinare `viewInsets` con il padding sicuro:\n\n```dart\nshowModalBottomSheet(\n  context: context,\n  isScrollControlled: true,\n  useSafeArea: true,\n  builder: (context) => Padding(\n    padding: EdgeInsets.only(\n      bottom: MediaQuery.viewInsetsOf(context).bottom,\n    ),\n    child: const _CommentForm(),\n  ),\n);\n```\n\nNel corpo della pagina, invece, `Scaffold.resizeToAvoidBottomInset` (attivo di default) ridimensiona il body all'apertura della tastiera. Disattivalo solo se gestisci gli inset manualmente, ad esempio con un `AnimatedPadding`.\n\n## Display cutout e schermi curvi\n\nPer i ritagli fisici puoi ispezionare `MediaQuery.of(context).displayFeatures` e `padding` in modalità landscape, dove il notch finisce sui lati. Un errore frequente è applicare `SafeArea` solo in verticale: in orizzontale `padding.left` o `padding.right` possono valere 40+ logical pixel. `SafeArea` senza parametri li gestisce tutti; se li disabiliti, ricordati di reintrodurli nel padding orizzontale dei contenuti.\n\n## Testare senza dieci dispositivi\n\nGli inset sono semplici da simulare nei widget test, perché basta avvolgere la UI in un `MediaQuery` con i valori desiderati:\n\n```dart\ntestWidgets('il CTA resta sopra la barra gestuale', (tester) async {\n  await tester.pumpWidget(\n    MediaQuery(\n      data: const MediaQueryData(\n        padding: EdgeInsets.only(top: 54, bottom: 34),\n        viewPadding: EdgeInsets.only(top: 54, bottom: 34),\n      ),\n      child: const MaterialApp(home: CheckoutPage()),\n    ),\n  );\n\n  final buttonRect = tester.getRect(find.byKey(const Key('cta')));\n  final screen = tester.view.physicalSize \u002F tester.view.devicePixelRatio;\n  expect(buttonRect.bottom, lessThanOrEqualTo(screen.height - 34));\n});\n```\n\nAggiungi un golden test con gli stessi inset e avrai una rete di sicurezza contro le regressioni di layout sui dispositivi con notch.\n\n## Checklist finale\n\n- Verifica l'app su Android 15 con **navigazione gestuale e a tre pulsanti**: gli inset inferiori sono molto diversi.\n- Non colorare più status bar e navigation bar: usa solo la brightness delle icone.\n- Usa `paddingOf`, `viewPaddingOf`, `viewInsetsOf` invece di `MediaQuery.of` per ridurre i rebuild.\n- Lascia scorrere i contenuti sotto le barre, ma aggiungi padding agli scroll view.\n- Controlla il contrasto dietro la barra gestuale prima di disattivare `systemNavigationBarContrastEnforced`.\n- Se hai codice nativo che chiama `setStatusBarColor`, rimuovilo: su API 35 non ha più effetto.\n\nAdottare correttamente l'edge-to-edge non è solo conformità alle regole di Android: è ciò che fa sembrare un'app Flutter davvero nativa su schermi moderni, dove il contenuto arriva fino al bordo e le barre di sistema diventano parte dell'interfaccia.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F125adfa1-3d13-4260-9a96-fc04cceff8ee.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1517777298614-cb6eefb19fad?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODk2MTc2ODR8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Nicolas Thomas","https:\u002F\u002Funsplash.com\u002F@nicolasthomas","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fshallow-focus-photography-of-space-gray-iphone-5s-PLDkBHbM3Hc",null,"published","2026-09-17T04:01:24+00:00","Edge-to-edge in Flutter: guida per Android 15 e iOS","Come gestire barre di sistema, notch e tastiera in Flutter con SafeArea, MediaQuery padding\u002FviewPadding e SystemUiOverlayStyle su Android 15 e iOS.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316910544]