[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-gestire-la-tastiera-in-flutter-insets-scroll-e-focus-senza-overflow":3,"comments-article-gestire-la-tastiera-in-flutter-insets-scroll-e-focus-senza-overflow":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},108,"Gestire la tastiera in Flutter: insets, scroll e focus senza overflow","gestire-la-tastiera-in-flutter-insets-scroll-e-focus-senza-overflow","La tastiera software è una delle cause più comuni di overflow e UI rotte in Flutter. Guida pratica a viewInsets, resizeToAvoidBottomInset, scroll automatico sul campo con focus, toolbar ancorate e bottom sheet che si comportano bene su Android e iOS.","## Perché la tastiera rompe le UI\n\nQuasi ogni app Flutter, prima o poi, mostra il famigerato nastro giallo e nero `BOTTOM OVERFLOWED BY 137 PIXELS`. Il colpevole è quasi sempre lo stesso: la tastiera software compare, riduce lo spazio disponibile e il layout non è preparato a gestirlo.\n\nIl problema è che la tastiera non è un widget: è una superficie di sistema che si sovrappone alla finestra dell'app. Flutter la espone solo come un valore numerico dentro `MediaQuery`, e sta a noi decidere come reagire. In questo articolo vediamo tutti gli strumenti disponibili, dai più semplici ai casi limite (toolbar ancorate, bottom sheet, form lunghi, edge-to-edge).\n\n## viewInsets, padding e viewPadding: la differenza che conta\n\n`MediaQueryData` espone tre proprietà che sembrano simili ma non lo sono:\n\n- **`viewInsets`**: lo spazio coperto da una superficie di sistema che *nasconde completamente* i contenuti. La tastiera vive qui: `viewInsets.bottom` vale 0 a tastiera chiusa e, ad esempio, 320 a tastiera aperta.\n- **`viewPadding`**: le aree occupate da notch, barre di sistema, home indicator. Non cambia quando appare la tastiera.\n- **`padding`**: come `viewPadding`, ma azzerato nelle zone già coperte da `viewInsets`. Quando la tastiera è aperta, `padding.bottom` diventa 0 perché la home indicator è sotto la tastiera. È il valore usato da `SafeArea`.\n\nLa regola pratica: **per reagire alla tastiera si usa sempre `viewInsets.bottom`**, mai `padding.bottom`.\n\nDa Flutter 3.10 esistono gli accessor granulari, che sono anche più efficienti perché il widget viene ricostruito solo quando quella specifica proprietà cambia:\n\n```dart\n\u002F\u002F Male: si ricostruisce a ogni cambiamento di MediaQuery (rotazione, text scale, ecc.)\nfinal inset = MediaQuery.of(context).viewInsets.bottom;\n\n\u002F\u002F Bene: dipendenza mirata\nfinal inset = MediaQuery.viewInsetsOf(context).bottom;\n```\n\n## resizeToAvoidBottomInset: cosa fa davvero\n\n`Scaffold` ha il flag `resizeToAvoidBottomInset`, attivo di default. Quando è `true`, lo Scaffold aggiunge un padding inferiore pari a `viewInsets.bottom` al proprio `body`: in pratica il corpo della pagina viene **rimpicciolito** per stare sopra la tastiera.\n\nQuesto comportamento è identico su Android e iOS (non dipende dal `windowSoftInputMode` nativo) e risolve il 90% dei casi, **a patto che il body sia scrollabile**. Se il body è una `Column` rigida, lo spazio si riduce ma il contenuto no: da qui l'overflow.\n\n```dart\nScaffold(\n  appBar: AppBar(title: const Text('Registrazione')),\n  body: SafeArea(\n    child: SingleChildScrollView(\n      padding: const EdgeInsets.all(16),\n      child: Column(\n        children: [\n          TextFormField(decoration: const InputDecoration(labelText: 'Nome')),\n          const SizedBox(height: 12),\n          TextFormField(decoration: const InputDecoration(labelText: 'Email')),\n          \u002F\u002F ...altri campi\n        ],\n      ),\n    ),\n  ),\n);\n```\n\nQuando invece il design richiede uno sfondo a tutto schermo o una mappa che non deve deformarsi, si disattiva il resize e si gestisce l'inset a mano:\n\n```dart\nScaffold(\n  resizeToAvoidBottomInset: false,\n  body: Stack(\n    children: [\n      const MappaFullScreen(),\n      Positioned(\n        left: 16,\n        right: 16,\n        bottom: 16 + MediaQuery.viewInsetsOf(context).bottom,\n        child: const BarraRicerca(),\n      ),\n    ],\n  ),\n);\n```\n\n## Il pattern \"Column con pulsante in fondo\"\n\nUn layout ricorrente: form in alto, pulsante di conferma ancorato in basso. Con `Expanded` dentro una `Column` non scrollabile l'overflow è garantito. La soluzione robusta è `LayoutBuilder` + `ConstrainedBox` + `IntrinsicHeight`:\n\n```dart\nLayoutBuilder(\n  builder: (context, constraints) {\n    return SingleChildScrollView(\n      child: ConstrainedBox(\n        constraints: BoxConstraints(minHeight: constraints.maxHeight),\n        child: IntrinsicHeight(\n          child: Column(\n            children: [\n              const CampiForm(),\n              const Spacer(), \u002F\u002F funziona grazie a IntrinsicHeight\n              const PulsanteConferma(),\n            ],\n          ),\n        ),\n      ),\n    );\n  },\n);\n```\n\nCon la tastiera chiusa il pulsante resta in fondo allo schermo; con la tastiera aperta il contenuto diventa scrollabile e nulla va in overflow. Attenzione: `IntrinsicHeight` ha un costo di layout non banale, quindi evitalo dentro liste lunghe o ricostruzioni frequenti.\n\nIn alternativa, se il pulsante deve restare sempre visibile *sopra* la tastiera, usa `bottomNavigationBar` dello Scaffold: viene posizionato correttamente rispetto agli insets.\n\n## Scroll automatico verso il campo con focus\n\nFlutter fa già qualcosa per te: `EditableText` chiama internamente `Scrollable.ensureVisible` quando un campo riceve il focus, usando la proprietà `scrollPadding` (di default `EdgeInsets.all(20)`). Se hai una toolbar personalizzata o un pulsante ancorato sopra la tastiera, quei 20 logical pixel non bastano:\n\n```dart\nTextField(\n  scrollPadding: const EdgeInsets.only(bottom: 120),\n  decoration: const InputDecoration(labelText: 'Note'),\n)\n```\n\nSe invece devi portare in vista un blocco più grande del singolo campo (per esempio una card che contiene campo + messaggio di errore), gestiscilo manualmente con un `FocusNode`:\n\n```dart\nclass CampoConScroll extends StatefulWidget {\n  const CampoConScroll({super.key, required this.label});\n  final String label;\n\n  @override\n  State\u003CCampoConScroll> createState() => _CampoConScrollState();\n}\n\nclass _CampoConScrollState extends State\u003CCampoConScroll> {\n  final _focusNode = FocusNode();\n\n  @override\n  void initState() {\n    super.initState();\n    _focusNode.addListener(_onFocusChange);\n  }\n\n  void _onFocusChange() {\n    if (!_focusNode.hasFocus) return;\n    \u002F\u002F Attende che il layout si sia aggiornato con la tastiera aperta\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (!mounted) return;\n      Scrollable.ensureVisible(\n        context,\n        alignment: 0.1, \u002F\u002F il widget si posiziona in alto nel viewport\n        duration: const Duration(milliseconds: 250),\n        curve: Curves.easeOut,\n      );\n    });\n  }\n\n  @override\n  void dispose() {\n    _focusNode.removeListener(_onFocusChange);\n    _focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TextField(\n      focusNode: _focusNode,\n      decoration: InputDecoration(labelText: widget.label),\n    );\n  }\n}\n```\n\nRicorda sempre di fare `dispose()` dei `FocusNode`: sono una delle fonti più comuni di memory leak nei form.\n\n## Passare da un campo all'altro\n\nPer una UX curata, il tasto di invio della tastiera deve portare al campo successivo e, sull'ultimo, inviare il form:\n\n```dart\nfinal _emailFocus = FocusNode();\nfinal _passwordFocus = FocusNode();\n\nColumn(\n  children: [\n    TextFormField(\n      focusNode: _emailFocus,\n      keyboardType: TextInputType.emailAddress,\n      textInputAction: TextInputAction.next,\n      autofillHints: const [AutofillHints.email],\n      onFieldSubmitted: (_) => _passwordFocus.requestFocus(),\n    ),\n    TextFormField(\n      focusNode: _passwordFocus,\n      obscureText: true,\n      textInputAction: TextInputAction.done,\n      autofillHints: const [AutofillHints.password],\n      onFieldSubmitted: (_) => _submit(),\n    ),\n  ],\n)\n```\n\nIn alternativa, `FocusScope.of(context).nextFocus()` sposta il focus seguendo l'ordine di traversal, senza dover dichiarare un nodo per ogni campo. Gli `autofillHints`, oltre a migliorare la compilazione automatica, fanno comparire sulla tastiera i suggerimenti di sistema e i password manager: avvolgi il form in un `AutofillGroup` per completezza.\n\nScegli inoltre il `keyboardType` corretto (`TextInputType.number`, `phone`, `emailAddress`, `TextInputType.numberWithOptions(decimal: true)`) e usa `inputFormatters` per filtrare l'input: su iOS la tastiera numerica non ha il tasto \"indietro\" del form, quindi valuta `TextInputAction.done`.\n\n## Chiudere la tastiera al momento giusto\n\nTre strategie complementari:\n\n```dart\n\u002F\u002F 1. Tocco fuori dai campi\nGestureDetector(\n  onTap: () => FocusManager.instance.primaryFocus?.unfocus(),\n  behavior: HitTestBehavior.opaque,\n  child: const ContenutoPagina(),\n);\n\n\u002F\u002F 2. Chiusura durante lo scroll (comportamento nativo iOS)\nListView.builder(\n  keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,\n  itemBuilder: (_, i) => ...,\n);\n\n\u002F\u002F 3. Chiusura programmatica prima di una navigazione\nvoid _submit() {\n  FocusScope.of(context).unfocus();\n  context.push('\u002Friepilogo');\n}\n```\n\nIl punto 3 è importante: se navighi con la tastiera aperta, la transizione risulta a scatti perché il layout della pagina uscente cambia durante l'animazione.\n\n## Bottom sheet, dialog e tastiera\n\nÈ il caso in cui sbagliare è più facile. Un `showModalBottomSheet` con un `TextField` deve essere `isScrollControlled: true` e applicare l'inset al proprio contenuto, usando il `context` **interno** al builder:\n\n```dart\nshowModalBottomSheet\u003Cvoid>(\n  context: context,\n  isScrollControlled: true,\n  useSafeArea: true,\n  builder: (sheetContext) {\n    return Padding(\n      padding: EdgeInsets.only(\n        left: 16,\n        right: 16,\n        top: 16,\n        bottom: 16 + MediaQuery.viewInsetsOf(sheetContext).bottom,\n      ),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          const TextField(autofocus: true),\n          const SizedBox(height: 12),\n          FilledButton(onPressed: () {}, child: const Text('Salva')),\n        ],\n      ),\n    );\n  },\n);\n```\n\nSenza `isScrollControlled: true` il sheet è limitato a metà schermo e la tastiera lo coprirebbe. Con `AnimatedPadding` al posto di `Padding` (durata ~200 ms, curva `decelerate`) il movimento risulta più morbido su Android, dove l'animazione della tastiera non è sincronizzata con quella di Flutter.\n\n## Toolbar ancorata sopra la tastiera\n\nPer barre di formattazione, contatori di caratteri o pulsanti \"Fatto\", il pattern è ancorare un widget all'`viewInsets.bottom` dentro uno `Stack`:\n\n```dart\nclass ToolbarTastiera extends StatelessWidget {\n  const ToolbarTastiera({super.key, required this.child});\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    final inset = MediaQuery.viewInsetsOf(context).bottom;\n    final visibile = inset > 0;\n\n    return AnimatedPositioned(\n      duration: const Duration(milliseconds: 150),\n      left: 0,\n      right: 0,\n      bottom: inset,\n      child: AnimatedOpacity(\n        duration: const Duration(milliseconds: 150),\n        opacity: visibile ? 1 : 0,\n        child: IgnorePointer(ignoring: !visibile, child: child),\n      ),\n    );\n  }\n}\n```\n\nSe hai bisogno di sapere *quando* la tastiera si apre o chiude al di fuori del `build` (per esempio per loggare un evento o mettere in pausa un'animazione), puoi osservare gli insets con un `WidgetsBindingObserver` e `didChangeMetrics`, oppure usare il pacchetto `flutter_keyboard_visibility` che espone uno `Stream\u003Cbool>`.\n\n## Edge-to-edge e Android moderno\n\nCon le app in modalità edge-to-edge (obbligatoria su Android 15 per le app che puntano a SDK 35) la gestione degli insets diventa più delicata: `SafeArea` protegge da barre di sistema e gesture bar, mentre la tastiera resta gestita da `viewInsets`. L'errore tipico è sommare due volte lo stesso spazio, usando sia `SafeArea` sia un padding manuale con `padding.bottom`. Ricorda che, a tastiera aperta, `MediaQuery.paddingOf(context).bottom` vale 0: se il tuo pulsante ancorato usa quel valore, non c'è rischio di doppio padding, ma se usi `viewPadding` sì.\n\n## Testare il comportamento\n\nNei widget test puoi simulare la tastiera sovrascrivendo il `MediaQuery`:\n\n```dart\ntestWidgets('il pulsante resta visibile con la tastiera aperta', (tester) async {\n  await tester.pumpWidget(\n    MaterialApp(\n      home: MediaQuery(\n        data: const MediaQueryData(\n          size: Size(390, 844),\n          viewInsets: EdgeInsets.only(bottom: 336),\n        ),\n        child: const PaginaLogin(),\n      ),\n    ),\n  );\n\n  expect(tester.takeException(), isNull); \u002F\u002F nessun overflow\n  expect(find.byType(FilledButton), findsOneWidget);\n});\n```\n\nIn debug, ogni overflow genera un'eccezione catturabile con `tester.takeException()`: è un modo economico per prevenire regressioni sui form principali.\n\n## Checklist finale\n\n- Usa `MediaQuery.viewInsetsOf(context)` e mai `padding.bottom` per la tastiera.\n- Rendi scrollabile ogni schermata che contiene campi di testo.\n- Disattiva `resizeToAvoidBottomInset` solo se gestisci gli insets a mano.\n- Imposta `scrollPadding` sui campi se hai elementi ancorati in basso.\n- Concatena i campi con `textInputAction` + `FocusNode` e aggiungi gli `autofillHints`.\n- Fai `dispose()` di `FocusNode` e `TextEditingController`.\n- Nei bottom sheet: `isScrollControlled: true` + padding con gli insets del context interno.\n- Chiudi la tastiera prima di navigare.\n\nGestire bene la tastiera non aggiunge funzionalità visibili, ma è esattamente il tipo di dettaglio che distingue un'app che \"sembra nativa\" da una che sembra un prototipo.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002Fe6ee2fee-145c-415a-9973-f23fd5e6f228.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1689348909596-8b747b9ff30f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTAzMDg5MDJ8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Di Nè!","https:\u002F\u002Funsplash.com\u002F@duytoo","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-close-up-of-a-cell-phone-in-the-dark-JwTiBTe7PkY",null,"published","2026-09-25T04:01:42+00:00","Gestire la tastiera in Flutter: guida pratica","Come gestire la tastiera software in Flutter: viewInsets, resizeToAvoidBottomInset, scroll sul campo con focus, toolbar ancorate e bottom sheet senza overflow.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316909136]