在〈頂端工具列 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。
TabBar 的 tabs 與 TabBarView 的 children 要能對應起來,必須透過 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)
],
);
}
}
在這個例子中,因為 AppBar 的 title 接受的是 Widget,我就直接把 TabBar 擺上去了,DefaultTabController 要設置 length,表示可用來管理三個分頁,來看一下切換效果:

如果不想共用 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 時,為什麼要指定 TickerProvider?TickerProvider 其實是給 TabController 內部的 AnimationController 使用的,要詳細談的話,是與動畫操作有關,如果不關心動畫,就像範例這麼撰寫就可以了。
如果想知道原理的話,之後談動畫會再說明,這邊先簡單地說一下,Flutter 基本上希望能在畫面顯示時,提供每秒 60 個畫框(frame)的更新,而 TickerProvider 可以提供 Ticker 實例,每次 Flutter 在更新畫框(Frame),會呼叫指定給 Ticker 的函式,這個函式中可以操作 AnimationController,例如更新它的 value,動畫相關的邏輯依 AnimationController 的 value 等資訊來產生動畫。
也就是說,每次更新畫框時要做什麼,是由 Ticker 來決定,而 TickerProvider 會提供 Ticker,TabController 需要 TickerProvider,只是為了能實現切換分頁的動畫罷了,從這點來看,曝露其實是個有點奇怪的設計,畢竟你可能並不怎麼關心動畫,覺得預設的就足夠了,然而實作上還是要撰寫 with SingleTickerProviderStateMixin 之類的程式碼。

