分頁工具列 TabBar


在〈頂端工具列 AppBar〉中的範例出現了 TabBar,也就是 Flutter 中提供的分頁元件,當時只是隨便塞到 bottom,實際上你按下各個 Tab 元件是不會動作的,因為沒有指定各個 Tab 要顯示哪個 Widget 作為分頁。

TabBar 可以透過 tabs,設置多個 Tab,至於各個 Tab 對應的 Widget,可以用 TabBarView 來組織,例如,若有三個分頁內容:

TabBarView(
  children: [
    Book(
      imgSrc: 'https://openhome.cc/Gossip/images/ACL059300.jpg',
      name: 'Java SE 14 技術手冊',
    ),
    Book(
      imgSrc: 'https://openhome.cc/Gossip/images/ACL054400.jpg',
      name: 'Python 3.7 技術手冊',
    ),
    Book(
      imgSrc: 'https://openhome.cc/Gossip/images/AEL022800.jpg',
      name: 'JavaScript 技術手冊',
    ),
  ],
)

接著就看你要將 TabBarView 擺在哪了,如果使用 Scaffold,通常會是擺到 body

TabBartabsTabBarViewchildren 要能對應起來,必須透過 TabController,如果應用程式的 UI 很單純,只會有一個 TabBar,最簡單的方式就是使用 DefaultTabController,這是個可以被子節點共用的 TabController,例如:

import 'package:flutter/material.dart';

void main() => runApp(
  MaterialApp(
    home: DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: TabBar(
            tabs: [
              Tab(text: 'Java'),
              Tab(text: 'Python'),
              Tab(text: 'JavaScript'),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            Book(
              imgSrc: 'https://openhome.cc/Gossip/images/ACL059300.jpg',
              name: 'Java SE 14 技術手冊',
            ),
            Book(
              imgSrc: 'https://openhome.cc/Gossip/images/ACL054400.jpg',
              name: 'Python 3.7 技術手冊',
            ),
            Book(
              imgSrc: 'https://openhome.cc/Gossip/images/AEL022800.jpg',
              name: 'JavaScript 技術手冊',
            ),
          ],
        ),
      ),
    )
  )
);

class Book extends StatelessWidget {
  final String imgSrc;
  final String name;

  Book({this.imgSrc, this.name});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Center(
          child: Image.network(imgSrc),
        ),
        Text(name)
      ],
    );
  }
}

在這個例子中,因為 AppBartitle 接受的是 Widget,我就直接把 TabBar 擺上去了,DefaultTabController 要設置 length,表示可用來管理三個分頁,來看一下切換效果:

分頁工具列 TabBar

如果不想共用 TabController 的話,也可以自行建立 TabController,例如以下的程式實作,執行結果同上,然而是自行建立 TabController

import 'package:flutter/material.dart';

void main() => runApp(
  MaterialApp(
    home: Home()
  )
);

class Home extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HomeState();
}

// mixin SingleTickerProviderStateMixin 的實作
// 讓這個類別能提供 Ticker,這是做動畫時需要的元件
class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
  // 宣告 TabController
  TabController tabController;

  @override
  void initState() {
    // 建立 TabController,vsync 接受的型態是 TickerProvider
    tabController = new TabController(length: 3, vsync: this);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: TabBar(
          tabs: [
            Tab(text: 'Java'),
            Tab(text: 'Python'),
            Tab(text: 'JavaScript'),
          ],
          controller: tabController,  // 指定 TabController
        ),
      ),
      body: TabBarView(
        controller: tabController,    // 指定 TabController
        children: [
          Book(
            imgSrc: 'https://openhome.cc/Gossip/images/ACL059300.jpg',
            name: 'Java SE 14 技術手冊',
          ),
          Book(
            imgSrc: 'https://openhome.cc/Gossip/images/ACL054400.jpg',
            name: 'Python 3.7 技術手冊',
          ),
          Book(
            imgSrc: 'https://openhome.cc/Gossip/images/AEL022800.jpg',
            name: 'JavaScript 技術手冊',
          ),
        ],
      ),
    );
  }
}

class Book extends StatelessWidget {
  同前一個範...略
}

基本上需注意的地方,範例程式碼中都加上了註解,至於建立 TabController 時,為什麼要指定 TickerProviderTickerProvider 其實是給 TabController 內部的 AnimationController 使用的,要詳細談的話,是與動畫操作有關,如果不關心動畫,就像範例這麼撰寫就可以了。

如果想知道原理的話,之後談動畫會再說明,這邊先簡單地說一下,Flutter 基本上希望能在畫面顯示時,提供每秒 60 個畫框(frame)的更新,而 TickerProvider 可以提供 Ticker 實例,每次 Flutter 在更新畫框(Frame),會呼叫指定給 Ticker 的函式,這個函式中可以操作 AnimationController,例如更新它的 value,動畫相關的邏輯依 AnimationControllervalue 等資訊來產生動畫。

也就是說,每次更新畫框時要做什麼,是由 Ticker 來決定,而 TickerProvider 會提供 TickerTabController 需要 TickerProvider,只是為了能實現切換分頁的動畫罷了,從這點來看,曝露其實是個有點奇怪的設計,畢竟你可能並不怎麼關心動畫,覺得預設的就足夠了,然而實作上還是要撰寫 with SingleTickerProviderStateMixin 之類的程式碼。