flutter實戰2:為APP增加上下Tab頁

燃燒的魚丸發表於2019-03-04

緊接上一篇的有側邊欄APP,這次我們向APP中加入上下Tab頁,使之跟趨近主流大部分APP的資訊佈局套路,等不及看原始碼的同學可以點選進入我的git倉庫下載程式碼。

Tab關鍵元素

  • TabController
    這是Tab頁的控制器,用於定義Tab標籤和內容頁的座標,還可配置標籤頁的切換動畫效果等。

TabController一般放入有狀態控制元件中使用,以適應標籤頁數量和內容有動態變化的場景,如果標籤頁在APP中是靜態固定的格局,則可以在無狀態控制元件中加入簡易版的DefaultTabController以提高執行效率,畢竟無狀態控制元件要比有狀態控制元件更省資源,執行效率更快。

  • TabBar
    Tab頁的Title控制元件,切換Tab頁的入口,一般放到AppBar控制元件下使用,內部有**Title*屬性。其子元素按水平橫向排列布局,如果需要縱向排列,請使用ColumnListView控制元件包裝一下。子元素為Tab型別的陣列。

  • TabBarView
    Tab頁的內容容器,其內放置Tab頁的主體內容。子元素可以是多個各種型別的控制元件。

Tab使用方法

有狀態控制元件搭配TabController

Tab頁的切換搭配了動畫,因此到State類上附加一個SingleTickerProviderStateMixin:

  //定義有狀態控制元件
  class HomePage extends StatefulWidget {
    @override
    _HomePageState createState() => new _HomePageState();
  }

  //用於使用到了一點點的動畫效果,因此加入了SingleTickerProviderStateMixin
  class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin{
    ...
  }
複製程式碼

然後到有狀態控制元件的子類State中初始化控制器TabController

  @override
  void initState() {
    super.initState();
    _tabController = new TabController(
      vsync: this,     //動畫效果的非同步處理,預設格式,背下來即可
      length: 3      //需要控制的Tab頁數量
    );    
  }
  //當整個頁面dispose時,記得把控制器也dispose掉,釋放記憶體
  @override
  void dispose() {
    _tabController .dispose();
    super.dispose();
  }
複製程式碼

然後到TabBarTabBarView中的controller屬性中呼叫控制器_tabController

  //標籤頁標題
  new TabBar(
          tabs: [    //注意TabBar的子元素為Tab型別的陣列
            new Tab(icon: new Icon(Icons.directions_car)),
            new Tab(icon: new Icon(Icons.directions_transit)),
            new Tab(icon: new Icon(Icons.directions_bike)),
          ]
  //標籤頁內容區域
  new TabBarView(
        children: [
          new Icon(Icons.directions_car),
          new Icon(Icons.directions_transit),
          new Icon(Icons.directions_bike),
        ]
複製程式碼

最後,我們把定義好的TabBarTabBarView安放到需要的地方去,比如:

new Scaffold(
      appBar: new AppBar(
        backgroundColor: Colors.deepOrange,
        title: new Text(`title`),
      ),
      ....
      body: new TabBarView(        //TabBarView呈現內容,因此放到Scaffold的body中
          controller: _bottomNavigation,      //配置控制器
          children:  [      //注意順序與TabBar保持一直
            new News(data: `引數值`),    //上一篇定義好的子頁面
            new TabPage2(),
            new TabPage3(),
          ]
        ),
      bottomNavigationBar: new Material(    //為了適配主題風格,包一層Material實現風格套用
        color: Colors.deepOrange,   //底部導航欄主題顏色
        child: new TabBar(        //TabBar導航標籤,底部導航放到Scaffold的bottomNavigationBar中
          controller: _bottomNavigation,      //配置控制器
          tabs: _bottomTabs,
          indicatorColor: Colors.white, //tab標籤的下劃線顏色
        ),
      ) 
    );
複製程式碼

無狀態控制元件搭配DefaultTabController

DefaultTabController要簡單很多,由於應用在無狀態控制元件中,使用DefaultTabController包裹需要用到Tab的頁面即可:

class TabPage3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return  new DefaultTabController(
        length: 3,
        child: new Scaffold(
          appBar: new AppBar(
            backgroundColor: Colors.orangeAccent,
            title: new TabBar(
              tabs: [
                new Tab(icon: new Icon(Icons.directions_car)),
                new Tab(icon: new Icon(Icons.directions_transit)),
                new Tab(icon: new Icon(Icons.directions_bike)),
              ],
              indicatorColor: Colors.white,
            ),
          ),
          body: new TabBarView(
            children: [
              new Icon(Icons.directions_car),
              new Icon(Icons.directions_transit),
              new Icon(Icons.directions_bike),
            ],
          ),
        ),
      );
  }
}
複製程式碼

DefaultTabControllerTabController的用法差異主要在控制器的定義上,TabBarTabBarView的使用方法完全相同,通常上下Tab頁的標籤分別安放在Scaffold控制元件的appBarbottomNavigationBar屬性上,然後我們把APP填充成下面這個樣式:

效果圖

程式碼結構

如上圖所示,APP以底部Tab導航欄為主入口,底部每個Tab中又各自有自己的頂部次級Tab頁,因此我們把程式碼結構整理一下:

程式碼框架
  • _HomePageState是APP的主頁面HomePage的子類State

  • 通過Scaffold底部的bottomNavigationBar屬性擺放TabBar,搭建Tab頁的標籤欄

  • 放入Scaffoldbody屬性放入TabBarViewTabBarViewchildren是三個外部dart檔案定義的控制元件頁面

  • 外部dart檔案定義的控制元件頁面分別又有各自風格的Tab標籤頁

  • Tab頁的通用屬性可以提前定義到陣列List中,在TabBarTabBarView通過遍歷提取List的值建立子元素可以提高程式碼的維護效率:

    //在StatefulWidget控制元件中,可通過修改下面的陣列,實現Tab頁的動態載入
    final List myTabs = [
    new Tab(text: `Tab1`),
    new Tab(text: `Tab2`),
    new Tab(text: `Tab3`),
    new Tab(text: `Tab4`),
    new Tab(text: `Tab5`),
    new Tab(text: `Tab6`),
    new Tab(text: `Tab7`),
    new Tab(text: `Tab8`),
    new Tab(text: `Tab9`),
    new Tab(text: `Tab10`),
    new Tab(text: `Tab11`),
    ];

    Widget build(BuildContext context) {
      return new Scaffold(
        appBar: new AppBar(
          backgroundColor: Colors.orangeAccent,
          title: new TabBar(
            controller: _tabController,
            tabs: myTabs,    //使用Tab型別的陣列呈現Tab標籤
            indicatorColor: Colors.white,
            isScrollable: true,   
          ),
        ),
        body: new TabBarView(
          controller: _tabController,
          children: myTabs.map((Tab tab) {    //遍歷List<Tab>型別的物件myTabs並提取其屬性值作為子控制元件的內容
            return new Center(child: new Text(tab.text+`   `+widget.data)); //使用引數值
          }).toList(),
        ),
      );
    }
    複製程式碼

使用獲取到的引數

由於StatelessWidgetStatefulWidget的頁面構建不同,使用從外部獲取到的引數的方式也略有差異,在這裡簡單總結下。

  • StatelessWidget的獲參和用參方式
    定義StatelessWidget控制元件時,新增一個final型別的變數如pageText,用於為引數值預留空間,並在建構函式中加入引數值:

    class SidebarPage extends StatelessWidget {
    final String pageText; //定義一個常量,用於儲存跳轉進來獲取到的引數
    SidebarPage(this.pageText); //建構函式,獲取引數

    }

使用引數時直接引用即可:

Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(title: new Text(pageText),),   //將引數當作頁面標題
      body: new Center(
        child: new Text(`pageText`),
      ),
    );
  }
複製程式碼

從外部傳入引數時,直接向建構函式中填入引數值即可:

Navigator.of(context).push(new MaterialPageRoute(builder: 
    (BuildContext context) => new SidebarPage(`First Page`)));    //在new方法時呼叫控制元件的建構函式傳入引數值
複製程式碼
  • StatefulWidget的獲參和用參方式
    相比StatelessWidget略複雜。定義建構函式時需要預設宣告key

    class TabPage1 extends StatefulWidget {
    const TabPage1({ Key key , this.data}) : super(key: key); //建構函式中增加引數
    final String data; //為引數分配空間
    @override
    _MyTabbedPageState createState() => new _MyTabbedPageState();
    }

使用時,由於在State子類中實現具體的頁面內容,因此State子類使用父類TabPage1的引數時需要在引數名前增加一個***widget***關鍵字:

class _MyTabbedPageState extends State<TabPage1> {
  ...
  new Center(child: new Text(tab.text+`   `+widget.data));   //使用引數值,需在引數名前增加widget字首
  ...
}
複製程式碼

從外部傳入引數時,需要宣告引數名:

new TabBarView
    controller: _bottomNavigation,
    children:  [      
      new TabPage1(data: `引數值`),    //new方法呼叫建構函式時,還需要宣告引數名稱
      new TabPage2(),
      new TabPage3(),
    ]
  )
複製程式碼

好勒,今天就講到這裡,大家去下載我的git原始碼試試效果吧,程式碼中有附加的註釋,對一些控制元件屬性的特性也有單獨的描述,相信看完原始碼之後,大家也可以自行實現效果了。

順便分享一個雷區,由於當初建立這個專案時,我使用命令**flutter create [APPname1]的方式建立了這個專案,但我發現這個APPname1(代稱,並非真實名稱)不好聽,想把專案改名為APPname2,於是參考之前寫的安卓怎麼打包?,把專案資料夾改名為APPname2,並非常任性的把專案目錄下的_androidappsrcmainAndroidManifest.xml_檔案,把packageandroid:label都改成了APPname2,於是不出意料的悲催了,命令flutter fun**報錯,無法啟動APP,還原配置之後,無法啟動APP,即便嘗試通過全文搜尋APPname1,都按規定格式替換成APPname2,或者逆向改回去,都無法啟動APP,此時已是凌晨1點。。。妥妥的血淚史,所以鄭重的告誡大家:

不要在專案的各種配置檔案中輕易改動專案名稱!不要在專案的各種配置檔案中輕易改動專案名稱!不要在專案的各種配置檔案中輕易改動專案名稱! 否則你就是下一個在電腦面前捶胸頓足的魚丸。什麼?問我怎麼恢復的?當然是託git的福。

感謝大家的支援,請關注我的Flutter圈子,多多投稿,也可以加入**flutter 中文社群(官方QQ群:338252156)**共同成長,謝謝大家~

相關文章