İçeriğe geç / Skip to content / Zum Inhalt

Flutter: TabBar und TabBarView verwenden

Ahmet Balaman

Zuletzt aktualisiert:

5 Min. Lesezeit

FlutterTabBarTabBarViewTabControllerNavigationWidget
Flutter: TabBar und TabBarView verwenden

TabBar und TabBarView zeigen gleichrangige Inhalte eines Screens (Aktiv / Abgeschlossen / Storniert) als nebeneinanderliegende Seiten. Gewechselt wird per Tipp auf einen Tab oder per Wischen nach links und rechts; das Bindeglied, das beide synchron hält, ist der TabController. Dort liegt auch die eigentliche Entscheidung: Soll Flutter den Controller verwalten oder Sie selbst? Dieser Beitrag behandelt diese Entscheidung, das Erhalten des Zustands von Tab-Inhalten und die Fehler, denen Sie am ehesten begegnen.

Live-Demo

Sie können dieses Widget im interaktiven Beispiel unten ausprobieren:

💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.

Einfache Verwendung mit DefaultTabController

Wechseln die Tabs nur den Inhalt, ist DefaultTabController der kürzeste Weg. Kein StatefulWidget, kein dispose; TabBar und TabBarView finden den Controller selbst im Baum:

DefaultTabController(
  length: 3,
  child: Scaffold(
    appBar: AppBar(
      title: const Text('Tabs'),
      bottom: const TabBar(
        tabs: [
          Tab(icon: Icon(Icons.home), text: 'Start'),
          Tab(icon: Icon(Icons.search), text: 'Suche'),
          Tab(icon: Icon(Icons.person), text: 'Profil'),
        ],
      ),
    ),
    body: const TabBarView(
      children: [
        Center(child: Text('Start')),
        Center(child: Text('Suche')),
        Center(child: Text('Profil')),
      ],
    ),
  ),
)

Beachten Sie, dass die TabBar im Bereich bottom der AppBar sitzt; ein Tab nimmt ein Icon, einen Text oder beides. Muss ein Widget weiter unten den Tab wechseln, genügt DefaultTabController.of(context).animateTo(2).

Einen eigenen TabController verwalten

Erzeugen Sie den Controller selbst, sobald etwas anderes vom aktiven Tab abhängt: einen FAB ausblenden, den Titel anpassen, einen Tab per Code wählen. Der Controller steuert eine Animation und verlangt deshalb ein vsync, das SingleTickerProviderStateMixin liefert:

TabBar und TabBarView binden beidseitig an einen TabController in der Mitte und bleiben über einen Index synchron

class _MyPageState extends State<MyPage>
    with SingleTickerProviderStateMixin {
  late final TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }
}

Denselben _tabController übergeben Sie als controller sowohl an die TabBar als auch an die TabBarView. Warum initState und dispose immer als Paar auftreten, erkläre ich im Beitrag zum Lebenszyklus.

// Zu einem bestimmten Tab wechseln
_tabController.animateTo(2);

// Index des aktiven Tabs
final current = _tabController.index;

TabBar-Eigenschaften

Eigenschaft Beschreibung
tabs Liste der Tab-Widgets
controller TabController (fehlt er, wird ein DefaultTabController gesucht)
isScrollable Aktiviert horizontales Scrollen, wenn die Tabs nicht passen
tabAlignment Ausrichtung der Tabs (start, center, fill, startOffset)
indicatorColor / indicatorWeight Farbe und Stärke der Unterstreichung
indicatorSize Ob die Linie den Tab oder nur das Label überspannt
labelColor / unselectedLabelColor Farben gewählter und nicht gewählter Tabs
dividerColor Die dünne Trennlinie unter der Leiste in Material 3

Scrollbare Tabs

Mehr als fünf oder sechs Tabs passen nicht auf den Bildschirm; mit isScrollable: true wird die Leiste horizontal scrollbar:

const TabBar(
  isScrollable: true,
  tabAlignment: TabAlignment.start,
  tabs: [
    Tab(text: 'Alle'),
    Tab(text: 'Flutter'),
    Tab(text: 'Swift'),
    Tab(text: 'Algorithmen'),
    Tab(text: '.NET'),
  ],
)

Die Zeile mit tabAlignment ist keine Zierde; warum, zeigt der Abschnitt zu den Fehlern.

Tab-Zustand erhalten

TabBarView entfernt nicht sichtbare Tabs aus dem Speicher. Scrollt der Nutzer in einer Liste nach unten, wechselt den Tab und kehrt zurück, steht die Liste wieder am Anfang; Formularfelder sind ebenfalls zurückgesetzt. Mit AutomaticKeepAliveClientMixin am State des Tabs verhindern Sie das:

class _OrderListState extends State<OrderList>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context); // vom Mixin vorgeschrieben
    return ListView.builder(
      itemCount: 30,
      itemBuilder: (context, index) => ListTile(
        title: Text('${widget.status}: Bestellung #${index + 1}'),
      ),
    );
  }
}

Hängen Sie das nicht reflexhaft an jeden Tab; jeder am Leben gehaltene Tab belegt Speicher. Setzen Sie es dort ein, wo der verlorene Zustand den Nutzer wirklich stört.

Wann verwenden – und wann nicht?

Tabs passen zu zwei bis sechs gleichrangigen Ansichten desselben Kontexts: die Zustände einer Bestellung, die Beiträge und Likes eines Profils.

  • Für die Hauptbereiche einer App (Start, Suche, Profil) nehmen Sie statt Tabs eine BottomNavigationBar; die untere Leiste liegt nah am Daumen und bleibt über Screens hinweg stehen.
  • Für einen einfachen Filter, der keine Seiten wechselt (Liste / Raster), ist SegmentedButton leichter.
  • Müssen Schritte der Reihe nach erledigt werden (Adresse, Zahlung, Bestätigung), senden Tabs das falsche Signal; nutzen Sie einen Stepper oder einen Ablauf Seite für Seite.
  • Eine horizontal wischbare Galerie oder eine Karte in einem Tab konkurriert mit der Wischgeste der TabBarView. Dann können Sie der TabBarView physics: const NeverScrollableScrollPhysics() geben und den Wechsel allein dem Tippen überlassen.

Häufige Fehler

1. No TabController for TabBar. Die TabBar hat keinen controller bekommen, und darüber gibt es keinen DefaultTabController. Umschließen Sie das Scaffold mit einem DefaultTabController oder übergeben Sie Ihren eigenen Controller an TabBar und TabBarView. Ihn nur an eines der beiden zu geben, führt zur gleichen Art von Fehler.

2. Controller's length property (3) does not match the number of tabs (2) present in TabBar's tabs property. Der Wert von length, die Anzahl der tabs und die Anzahl der children in der TabBarView müssen übereinstimmen. Passen Sie beim Hinzufügen eines Tabs alle drei Stellen an; die Zahl aus einer Liste abzuleiten (length: _tabs.length) beseitigt das Problem an der Wurzel.

3. dispose vergessen oder falsches Mixin. Ohne dispose werden Ticker und Listener des Controllers nie freigegeben und bleiben im Speicher, auch wenn die Seite längst weg ist. Hält derselbe State zusätzlich einen zweiten AnimationController, scheitert SingleTickerProviderStateMixin mit „multiple tickers were created“; wechseln Sie dann zu TickerProviderStateMixin.

4. Der Listener feuert zweimal. Beim Tipp auf einen Tab läuft der Callback von addListener sowohl zu Beginn als auch am Ende der Animation. Für einmalige Arbeit filtern Sie mit indexIsChanging:

void _onTabChanged() {
  if (_tabController.indexIsChanging) return; // erst nach der Animation
  setState(() {});
}

5. Lücke links neben scrollbaren Tabs in Material 3. Bei isScrollable: true ist die Standardausrichtung TabAlignment.startOffset, die den ersten Tab einrückt. Sollen die Tabs am Rand beginnen, schreiben Sie tabAlignment: TabAlignment.start. Auch die dünne Linie unter der Leiste stammt aus Material 3; dividerColor: Colors.transparent entfernt sie.

Mini-Szenario: Ein Bestellungen-Screen

Der Screen „Meine Bestellungen“ soll drei Tabs haben. Der Button „Neue Bestellung“ erscheint nur im Tab „Aktiv“, und wenn der Nutzer den Tab wechselt und zurückkehrt, steht die Liste dort, wo er sie verlassen hat. Die erste Anforderung verlangt einen eigenen Controller, die zweite Keep-Alive:

class OrdersPage extends StatefulWidget {
  const OrdersPage({super.key});

  @override
  State<OrdersPage> createState() => _OrdersPageState();
}

class _OrdersPageState extends State<OrdersPage>
    with SingleTickerProviderStateMixin {
  late final TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    _tabController.addListener(_onTabChanged);
  }

  void _onTabChanged() {
    if (_tabController.indexIsChanging) return; // erst nach der Animation
    setState(() {}); // Sichtbarkeit des FAB aktualisieren
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Meine Bestellungen'),
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: 'Aktiv'),
            Tab(text: 'Abgeschlossen'),
            Tab(text: 'Storniert'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: const [
          OrderList(status: 'Aktiv'),
          OrderList(status: 'Abgeschlossen'),
          OrderList(status: 'Storniert'),
        ],
      ),
      floatingActionButton: _tabController.index == 0
          ? FloatingActionButton(
              onPressed: () {},
              child: const Icon(Icons.add),
            )
          : null,
    );
  }
}

OrderList ist das Widget aus dem Keep-Alive-Abschnitt oben. Der Listener ruft setState erst auf, wenn der Wechsel abgeschlossen ist; der FloatingActionButton ändert sich also, wenn der neue Tab steht, nicht mitten in der Animation. Beendet dispose den Controller, verschwindet der daran hängende Listener gleich mit.

Häufig gestellte Fragen

DefaultTabController oder ein eigener TabController?

Wechseln die Tabs nur den Inhalt, reicht DefaultTabController und braucht weniger Code. Erzeugen Sie einen eigenen TabController, wenn ein anderer Teil der Oberfläche vom aktiven Index abhängt oder Sie Tabs per Code auswählen.

Wie deaktiviere ich das Wischen zwischen Tabs?

Geben Sie der TabBarView physics: const NeverScrollableScrollPhysics(). Der Wechsel per Tipp auf einen Tab funktioniert weiterhin.

Warum wird der Inhalt beim Tab-Wechsel zurückgesetzt?

TabBarView nimmt unsichtbare Seiten aus dem Baum, und ihr State geht mit. Ergänzen Sie die State-Klasse des Tabs um AutomaticKeepAliveClientMixin und geben Sie bei wantKeepAlive true zurück.

Kann ich eine TabBar außerhalb der AppBar verwenden?

Ja. TabBar ist ein gewöhnliches Widget; Sie können sie in eine Column setzen und darunter eine in Expanded verpackte TabBarView platzieren. Beide müssen nur denselben Controller sehen.

Kommentare