[{"data":1,"prerenderedAt":79},["ShallowReactive",2],{"tutorial-creare-una-barra-di-navigazione-inferiore-in-flutter-con-bottomnavigationbar-e-indexedstack":3,"comments-tutorial-creare-una-barra-di-navigazione-inferiore-in-flutter-con-bottomnavigationbar-e-indexedstack":78},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":9,"cover_credit":9,"video_url":10,"video":11,"difficulty":15,"estimated_minutes":16,"flutter_version":17,"status":18,"published_at":19,"meta_title":20,"meta_description":21,"category":22,"author":26,"steps":28},83,"Creare una barra di navigazione inferiore in Flutter con BottomNavigationBar e IndexedStack","creare-una-barra-di-navigazione-inferiore-in-flutter-con-bottomnavigationbar-e-indexedstack","Impara a costruire la classica navigazione a tab in fondo allo schermo: BottomNavigationBar, gestione dell'indice con setState, IndexedStack per non perdere lo stato e la nuova NavigationBar di Material 3.","La **barra di navigazione inferiore** è uno dei pattern più diffusi nelle app mobile: due o più sezioni principali (Home, Ricerca, Profilo...) sempre raggiungibili con un tocco.\n\nIn Flutter questo pattern si realizza in pochissime righe grazie al widget `BottomNavigationBar`, che si aggancia alla proprietà `bottomNavigationBar` dello `Scaffold`.\n\nIn questo tutorial partiamo da zero e costruiamo passo passo un'app con tre sezioni. Vedremo:\n\n- come collegare la barra a un indice di stato con `setState`;\n- perché usare `IndexedStack` invece di ricostruire ogni volta la pagina;\n- come **preservare lo scroll** e lo stato dei campi di testo passando da un tab all'altro;\n- come personalizzare colori, icone e badge;\n- come passare alla nuova `NavigationBar` di **Material 3**.\n\n> Prerequisiti: Flutter installato e un progetto vuoto creato con `flutter create`. Serve solo il framework, nessun pacchetto esterno.",null,"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=1yCnLbBfjZw",{"youtube_id":12,"duration_seconds":13,"published_at":14},"1yCnLbBfjZw",143,"2026-09-03T09:18:46+00:00","beginner",35,"3.x","published","2026-09-03T04:31:43+00:00","BottomNavigationBar in Flutter: guida pratica","Guida beginner alla navigazione a tab in Flutter: BottomNavigationBar, IndexedStack per mantenere lo stato e NavigationBar di Material 3.",{"id":23,"name":24,"slug":25},1,"Guide","guide",{"id":23,"name":27},"Flutter Bot",[29,36,43,50,57,64,71],{"id":30,"position":23,"title":31,"body":32,"code_snippet":33,"code_language":34,"expected_result":35,"demo_url":9,"video_url":9},553,"Preparare il progetto e lo scheletro dell'app","Crea un nuovo progetto (o svuota `lib\u002Fmain.dart` di uno esistente):\n\n```bash\nflutter create nav_demo\ncd nav_demo\n```\n\nSostituisci il contenuto di `lib\u002Fmain.dart` con lo scheletro qui sotto. Per ora abbiamo solo un `MaterialApp` con il tema Material 3 attivo e una schermata principale ancora vuota, che diventerà il **contenitore** della nostra navigazione.\n\nNota il widget `HomeShell`: è uno `StatefulWidget` perché dovrà ricordare **quale tab è selezionato**. Questa è la prima decisione importante: l'indice del tab è uno stato locale della schermata, non serve nessun package di state management.","import 'package:flutter\u002Fmaterial.dart';\n\nvoid main() => runApp(const MyApp());\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'Nav Demo',\n      debugShowCheckedModeBanner: false,\n      theme: ThemeData(\n        colorSchemeSeed: Colors.indigo,\n        useMaterial3: true,\n      ),\n      home: const HomeShell(),\n    );\n  }\n}\n\nclass HomeShell extends StatefulWidget {\n  const HomeShell({super.key});\n\n  @override\n  State\u003CHomeShell> createState() => _HomeShellState();\n}\n\nclass _HomeShellState extends State\u003CHomeShell> {\n  int _currentIndex = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return const Scaffold(\n      body: Center(child: Text('Contenuto in arrivo')),\n    );\n  }\n}","dart","L'app si avvia mostrando una schermata bianca con la scritta \"Contenuto in arrivo\". Nessun errore in console.",{"id":37,"position":38,"title":39,"body":40,"code_snippet":41,"code_language":34,"expected_result":42,"demo_url":9,"video_url":9},554,2,"Creare le tre pagine da mostrare","Ogni tab mostrerà una pagina diversa. Creiamo tre widget semplici ma **non banali**: la Home contiene una lista scrollabile (ci servirà per verificare il mantenimento dello scroll), la Ricerca un `TextField`, il Profilo del testo statico.\n\nPuoi metterli in fondo a `main.dart` per comodità; in un progetto reale andrebbero in file separati dentro `lib\u002Fpages\u002F`.\n\nOsserva che `SearchPage` è `StatefulWidget` con un `TextEditingController`: nel passo 4 vedremo cosa succede al testo digitato quando cambiamo tab.","class HomePage extends StatelessWidget {\n  const HomePage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView.builder(\n      itemCount: 50,\n      itemBuilder: (context, i) => ListTile(\n        leading: CircleAvatar(child: Text('${i + 1}')),\n        title: Text('Elemento numero ${i + 1}'),\n        subtitle: const Text('Scorri la lista e cambia tab'),\n      ),\n    );\n  }\n}\n\nclass SearchPage extends StatefulWidget {\n  const SearchPage({super.key});\n\n  @override\n  State\u003CSearchPage> createState() => _SearchPageState();\n}\n\nclass _SearchPageState extends State\u003CSearchPage> {\n  final _controller = TextEditingController();\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(16),\n      child: TextField(\n        controller: _controller,\n        decoration: const InputDecoration(\n          labelText: 'Cerca',\n          prefixIcon: Icon(Icons.search),\n          border: OutlineInputBorder(),\n        ),\n      ),\n    );\n  }\n}\n\nclass ProfilePage extends StatelessWidget {\n  const ProfilePage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const Center(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          CircleAvatar(radius: 40, child: Icon(Icons.person, size: 40)),\n          SizedBox(height: 12),\n          Text('Mario Rossi', style: TextStyle(fontSize: 20)),\n        ],\n      ),\n    );\n  }\n}","Il codice compila senza errori: le tre pagine esistono ma non sono ancora visibili nell'app.",{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":34,"expected_result":49,"demo_url":9,"video_url":9},555,3,"Aggiungere la BottomNavigationBar e collegarla all'indice","Ora colleghiamo tutto. Nello `Scaffold` valorizziamo la proprietà `bottomNavigationBar` con un `BottomNavigationBar` che riceve:\n\n- `currentIndex`: l'indice del tab attivo (il nostro stato);\n- `onTap`: callback chiamata al tocco, dove aggiorniamo lo stato con `setState`;\n- `items`: la lista dei `BottomNavigationBarItem` (**minimo 2**, altrimenti Flutter lancia un errore).\n\nPer il `body` usiamo per ora un semplice accesso alla lista di pagine tramite indice.\n\n⚠️ **Attenzione a un dettaglio classico**: con più di 3 item la barra passa automaticamente al tipo `shifting` e, se non imposti i colori, le etichette non selezionate spariscono. Puoi forzare il comportamento con `type: BottomNavigationBarType.fixed`.","class _HomeShellState extends State\u003CHomeShell> {\n  int _currentIndex = 0;\n\n  final _pages = const [\n    HomePage(),\n    SearchPage(),\n    ProfilePage(),\n  ];\n\n  final _titles = const ['Home', 'Ricerca', 'Profilo'];\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text(_titles[_currentIndex])),\n      body: _pages[_currentIndex],\n      bottomNavigationBar: BottomNavigationBar(\n        currentIndex: _currentIndex,\n        onTap: (index) => setState(() => _currentIndex = index),\n        type: BottomNavigationBarType.fixed,\n        items: const [\n          BottomNavigationBarItem(\n            icon: Icon(Icons.home_outlined),\n            activeIcon: Icon(Icons.home),\n            label: 'Home',\n          ),\n          BottomNavigationBarItem(\n            icon: Icon(Icons.search_outlined),\n            activeIcon: Icon(Icons.search),\n            label: 'Ricerca',\n          ),\n          BottomNavigationBarItem(\n            icon: Icon(Icons.person_outline),\n            activeIcon: Icon(Icons.person),\n            label: 'Profilo',\n          ),\n        ],\n      ),\n    );\n  }\n}","Toccando le tre icone in basso cambiano sia il contenuto sia il titolo nella AppBar. Se però scorri la lista della Home e torni indietro, la lista riparte dall'alto e il testo digitato nella Ricerca è sparito.",{"id":51,"position":52,"title":53,"body":54,"code_snippet":55,"code_language":34,"expected_result":56,"demo_url":9,"video_url":9},556,4,"Mantenere lo stato delle pagine con IndexedStack","Il problema visto nel passo precedente è normale: scrivendo `body: _pages[_currentIndex]` Flutter **rimuove dall'albero** la pagina precedente e con essa il suo `State` (posizione di scroll, testo dei campi, dati caricati).\n\nLa soluzione più semplice è `IndexedStack`: costruisce **tutti i figli** e ne mostra solo uno, mantenendo gli altri vivi ma invisibili.\n\nSostituisci la riga del `body`:\n\n```dart\nbody: IndexedStack(\n  index: _currentIndex,\n  children: _pages,\n),\n```\n\n**Pro**: stato preservato, cambio tab istantaneo.\n**Contro**: tutte le pagine vengono costruite subito. Se una pagina è molto pesante o fa chiamate di rete all'avvio, valuta il *lazy loading* (costruire la pagina solo la prima volta che viene aperta) come nell'esempio avanzato in fondo al codice.","@override\nWidget build(BuildContext context) {\n  return Scaffold(\n    appBar: AppBar(title: Text(_titles[_currentIndex])),\n    body: IndexedStack(\n      index: _currentIndex,\n      children: _pages,\n    ),\n    bottomNavigationBar: BottomNavigationBar(\n      currentIndex: _currentIndex,\n      onTap: (index) => setState(() => _currentIndex = index),\n      type: BottomNavigationBarType.fixed,\n      items: const [\n        BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'),\n        BottomNavigationBarItem(icon: Icon(Icons.search_outlined), activeIcon: Icon(Icons.search), label: 'Ricerca'),\n        BottomNavigationBarItem(icon: Icon(Icons.person_outline), activeIcon: Icon(Icons.person), label: 'Profilo'),\n      ],\n    ),\n  );\n}","Scorri la lista della Home, vai su Ricerca, scrivi qualcosa e torna alla Home: la posizione di scroll è mantenuta e, rientrando in Ricerca, il testo digitato è ancora lì.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":34,"expected_result":63,"demo_url":9,"video_url":9},557,5,"Personalizzare colori, badge ed evitare i tocchi inutili","Rendiamo la barra più curata:\n\n- `selectedItemColor` \u002F `unselectedItemColor` per i colori delle icone e delle etichette;\n- `showUnselectedLabels: false` per lo stile \"solo icona\" sui tab non attivi;\n- `Badge` (widget nativo da Flutter 3.7) per mostrare un contatore di notifiche;\n- un piccolo controllo in `onTap` per **non chiamare setState** se l'utente tocca il tab già attivo (evita rebuild inutili) e, opzionalmente, riportare la lista in cima al doppio tocco.\n\nAggiorna il metodo `onTap` e gli `items` come nello snippet.","int _notifiche = 3;\n\nvoid _onTabTapped(int index) {\n  if (index == _currentIndex) return; \u002F\u002F già selezionato: niente rebuild\n  setState(() => _currentIndex = index);\n}\n\n\u002F\u002F ...dentro build:\nbottomNavigationBar: BottomNavigationBar(\n  currentIndex: _currentIndex,\n  onTap: _onTabTapped,\n  type: BottomNavigationBarType.fixed,\n  backgroundColor: Colors.white,\n  selectedItemColor: Theme.of(context).colorScheme.primary,\n  unselectedItemColor: Colors.grey.shade600,\n  showUnselectedLabels: true,\n  selectedFontSize: 12,\n  unselectedFontSize: 12,\n  items: [\n    const BottomNavigationBarItem(\n      icon: Icon(Icons.home_outlined),\n      activeIcon: Icon(Icons.home),\n      label: 'Home',\n    ),\n    BottomNavigationBarItem(\n      icon: Badge.count(\n        count: _notifiche,\n        isLabelVisible: _notifiche > 0,\n        child: const Icon(Icons.search_outlined),\n      ),\n      activeIcon: const Icon(Icons.search),\n      label: 'Ricerca',\n    ),\n    const BottomNavigationBarItem(\n      icon: Icon(Icons.person_outline),\n      activeIcon: Icon(Icons.person),\n      label: 'Profilo',\n    ),\n  ],\n),","La barra mostra i colori del tema, un badge rosso con il numero 3 sull'icona Ricerca e non ricostruisce l'interfaccia quando tocchi il tab già selezionato.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":34,"expected_result":70,"demo_url":9,"video_url":9},558,6,"Passare alla NavigationBar di Material 3","Con Material 3 il widget consigliato è `NavigationBar` (al posto di `BottomNavigationBar`): è più alta, ha l'indicatore a pillola dietro l'icona selezionata e un'animazione di transizione integrata.\n\nLe differenze principali nell'API:\n\n| BottomNavigationBar | NavigationBar |\n|---|---|\n| `currentIndex` | `selectedIndex` |\n| `onTap` | `onDestinationSelected` |\n| `items` con `BottomNavigationBarItem` | `destinations` con `NavigationDestination` |\n| `activeIcon` | `selectedIcon` |\n\nSostituisci il blocco `bottomNavigationBar` con lo snippet qui sotto: il resto dell'app (stato, `IndexedStack`, pagine) resta identico.\n\nPuoi anche personalizzare l'aspetto globalmente nel tema con `navigationBarTheme: NavigationBarThemeData(...)`.","bottomNavigationBar: NavigationBar(\n  selectedIndex: _currentIndex,\n  onDestinationSelected: _onTabTapped,\n  labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,\n  destinations: [\n    const NavigationDestination(\n      icon: Icon(Icons.home_outlined),\n      selectedIcon: Icon(Icons.home),\n      label: 'Home',\n    ),\n    NavigationDestination(\n      icon: Badge.count(\n        count: _notifiche,\n        isLabelVisible: _notifiche > 0,\n        child: const Icon(Icons.search_outlined),\n      ),\n      selectedIcon: const Icon(Icons.search),\n      label: 'Ricerca',\n    ),\n    const NavigationDestination(\n      icon: Icon(Icons.person_outline),\n      selectedIcon: Icon(Icons.person),\n      label: 'Profilo',\n    ),\n  ],\n),","La barra assume l'aspetto Material 3, con l'indicatore a pillola che scorre sull'icona selezionata e la stessa logica di cambio pagina.",{"id":72,"position":73,"title":74,"body":75,"code_snippet":76,"code_language":34,"expected_result":77,"demo_url":9,"video_url":9},559,7,"Bonus: pagine con Navigator indipendente per ogni tab","Nelle app reali ogni tab ha spesso una **propria pila di navigazione**: dalla Home apri il dettaglio di un elemento e, cambiando tab e tornando indietro, ti aspetti di ritrovare il dettaglio aperto.\n\nSi ottiene avvolgendo ogni pagina in un `Navigator` con la sua `GlobalKey`, e gestendo il tasto Indietro di Android con `PopScope`: se la pila interna può fare `pop`, lo facciamo lì invece di chiudere l'app.\n\nÈ un pattern un po' più avanzato: se ti serve una gestione completa (deep link inclusi) valuta un router come `go_router` con le `StatefulShellRoute`, ma per un'app semplice questo approccio basta e non richiede dipendenze.","final _navKeys = List.generate(3, (_) => GlobalKey\u003CNavigatorState>());\n\nWidget _buildTabNavigator(int index, Widget child) {\n  return Navigator(\n    key: _navKeys[index],\n    onGenerateRoute: (settings) => MaterialPageRoute(\n      builder: (_) => child,\n      settings: settings,\n    ),\n  );\n}\n\n@override\nWidget build(BuildContext context) {\n  return PopScope(\n    canPop: false,\n    onPopInvokedWithResult: (didPop, result) {\n      if (didPop) return;\n      final nav = _navKeys[_currentIndex].currentState!;\n      if (nav.canPop()) {\n        nav.pop();\n      } else if (_currentIndex != 0) {\n        setState(() => _currentIndex = 0);\n      }\n    },\n    child: Scaffold(\n      body: IndexedStack(\n        index: _currentIndex,\n        children: [\n          _buildTabNavigator(0, const HomePage()),\n          _buildTabNavigator(1, const SearchPage()),\n          _buildTabNavigator(2, const ProfilePage()),\n        ],\n      ),\n      bottomNavigationBar: NavigationBar(\n        selectedIndex: _currentIndex,\n        onDestinationSelected: _onTabTapped,\n        destinations: const [\n          NavigationDestination(icon: Icon(Icons.home), label: 'Home'),\n          NavigationDestination(icon: Icon(Icons.search), label: 'Ricerca'),\n          NavigationDestination(icon: Icon(Icons.person), label: 'Profilo'),\n        ],\n      ),\n    ),\n  );\n}","Ogni tab mantiene la propria cronologia: aprendo una schermata di dettaglio dalla Home e cambiando tab, al ritorno la trovi ancora aperta. Il tasto Indietro chiude prima le schermate interne e poi riporta al primo tab.",[],1789205523946]