從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建

yuxiyu發表於2018-07-26

上一篇中我記錄了Flutter中常用的一些佈局,本篇開始開發基於Flutter的開源中國客戶端了。在本篇部落格中,要實現的是一個App的整體框架,包括頁面底部的Tab導航選單、頁面的側滑選單以及跳轉到新的頁面這幾個功能。希望自己在記錄的同時能溫故知新,同時給初學者一些幫助。

索引 文章
1 從0開始寫一個基於Flutter的開源中國客戶端(1)
Flutter簡介及開發環境搭建 | 掘金技術徵文
2 從0開始寫一個基於Flutter的開源中國客戶端(2)
Dart語法基礎
3 從0開始寫一個基於Flutter的開源中國客戶端(3)
初識Flutter & 常用的Widgets
4 從0開始寫一個基於Flutter的開源中國客戶端(4)
Flutter佈局基礎
?5 從0開始寫一個基於Flutter的開源中國客戶端(5)
App整體佈局框架搭建
6 從0開始寫一個基於Flutter的開源中國客戶端(6)
各個靜態頁面的實現
7 從0開始寫一個基於Flutter的開源中國客戶端(7)
App網路請求和資料儲存
8 從0開始寫一個基於Flutter的開源中國客戶端(8)
外掛的使用

App整體佈局框架搭建

在我們日常生活中經常使用的App比如微信、微博、QQ等,基本上都是使用首頁底部多個Tab可切換頁面,加上可側滑的選單這種佈局方式來組合。基於Flutter的開源中國客戶端也是使用這種佈局組合來實現的App。本篇要實現的頁面效果如下圖所示:

從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建 從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建 從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建

下面一步步來完成這個佈局框架的搭建。

新建專案

在AndroidStudio中,通過File -> New -> New Flutter Project...建立一個新的Flutter工程。

使用MaterialApp和Scaffold元件構建首頁

在新建立的Flutter工程中,刪除lib/main.dart中的程式碼,並編寫下面的程式碼:

import 'package:flutter/material.dart';

void main() {
  runApp(new MyApp());
}

// MyApp是一個有狀態的元件,因為頁面標題,頁面內容和頁面底部Tab都會改變
class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => new MyOSCClientState();
}

class MyOSCClientState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      theme: new ThemeData(
        // 設定頁面的主題色
        primaryColor: const Color(0xFF63CA6C)
      ),
      home: new Scaffold(
        appBar: new AppBar(
          // 設定AppBar標題
          title: new Text("My OSC",
            // 設定AppBar上文字的樣式
            style: new TextStyle(color: Colors.white)
          ),
          // 設定AppBar上圖示的樣式
          iconTheme: new IconThemeData(color: Colors.white)
        ),
        body: new Text("MyOSC Client")
      ),
    );
  }
}
複製程式碼

上面的程式碼中,為MaterialApp設定了theme引數,主要是為了改變頁面主題顏色為綠色,在Scaffold的appBar屬性中,為title設定了顏色為白色,如果不設定的話,預設為黑色,appBariconTheme屬性也設定為了白色主題,如果不設定的話,AppBar上的圖示預設為黑色。

編寫4個頁面用於切換顯示

在新建的Flutter專案的lib/目錄下,新建一個pages/目錄,該目錄用於存放App中的所有頁面,然後分別建立四個.dart檔案:NewsListPage.dart TweetsListPage.dart DiscoveryPage.dart MyInfoPage.dart,代表App中首頁底部4個Tab切換時分別顯示的頁面,這四個頁面暫時就在頁面正中間顯示一行文字,下面是資訊列表NewsListPage.dart程式碼:

// pages/NewsListPage.dart
import 'package:flutter/material.dart';

// 資訊列表頁面
class NewsListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Center(
      child: new Text("NewsListPage"),
    );
  }
}
複製程式碼

其餘三個頁面程式碼跟上面的類似,只是換了類名和Text元件的文字。

在上一步中,我們的Scaffold元件裡的body屬性只是一個Text元件,為了載入上面的4個頁面,需要用一個容器元件將這4個頁面裝起來,然後在點選Tab時切換頁面,這就用到了我之前的博文裡說到的IndexedStack元件了。IndexedStack中可以有多個子元件,根據索引值來顯示其中某個元件而隱藏其餘的元件。

在第一步中MyOSCClientState類中定義兩個變數:_tabIndex_body_tabIndex表示當前頁面底部選中的Tab的索引,_body表示首頁Scaffold元件的body屬性值,然後給_tabIndex_body變數賦值,如下程式碼所示:

// 頁面當前選中的Tab的索引
int _tabIndex = 0;

// 頁面body部分元件
var _body = new IndexedStack(
  children: <Widget>[
    new NewsListPage(),
    new TweetsListPage(),
    new DiscoveryPage(),
    new MyInfoPage()
  ],
  index: _tabIndex,
);
複製程式碼

上面用IndexedStack載入了4個頁面用於切換Tab時顯示,但是Tab我們還沒有做出來,Flutter中為頁面新增底部導航Tab選單很簡單,已經有很多元件可以用了。

編寫頁面底部導航Tab選單

給頁面新增底部導航Tab選單隻需要給Scaffold元件新增一個bottomNavigationBar屬性即可,這裡的bottomNavigationBar我們用Flutter提供的CupertinoTabBar元件。

CupertinoTabBar是Flutter內建的iOS風格的選項卡,用於在頁面底部顯示幾個Tab,要使用Cupertino風格的元件,必須先匯入標頭檔案,如下程式碼:

import 'package:flutter/cupertino.dart';
複製程式碼

CupertinoTabBar元件的用法也比較簡單,程式碼如下:

new CupertinoTabBar(
  items: getBottomNavItems(),
  currentIndex: _tabIndex,
  onTap: (index) {
    // 底部TabItem的點選事件處理,點選時改變當前選擇的Tab的索引值,則頁面會自動重新整理
    setState((){
      _tabIndex = index;
    });
  },
)
複製程式碼

其中items是一個List<BottomNavigationBarItem>物件,currentIndex表示當前選中的Tab的索引值,onTap是TabItem點選事件,上面的程式碼中,getBottomNavItems()方法程式碼如下:

List<BottomNavigationBarItem> getBottomNavItems() {
  List<BottomNavigationBarItem> list = new List();
  for (int i = 0; i < 4; i++) {
    list.add(new BottomNavigationBarItem(
      icon: getTabIcon(i),
      title: getTabTitle(i)
    ));
  }
  return list;
}

// 根據索引值確定Tab是選中狀態的樣式還是非選中狀態的樣式
TextStyle getTabTextStyle(int curIndex) {
  if (curIndex == _tabIndex) {
    return tabTextStyleSelected;
  }
  return tabTextStyleNormal;
}

// 根據索引值確定TabItem的icon是選中還是非選中
Image getTabIcon(int curIndex) {
  if (curIndex == _tabIndex) {
    return tabImages[curIndex][1];
  }
  return tabImages[curIndex][0];
}

// 根據索引值返回頁面頂部標題
Text getTabTitle(int curIndex) {
  return new Text(
    appBarTitles[curIndex],
    style: getTabTextStyle(curIndex)
  );
}
複製程式碼

由於TabItem是由一個圖示和一個文字元件構成,所以這裡還需要在MyOSCClientState類中定義兩個變數tabImagesappBarTitlestabImages是一個二維陣列,表示TabItem中的圖示(包括選中和未選中狀態的圖示),appBarTitles是一個字串陣列,表示每個TabItem對應的頁面標題,這兩個變數的賦值程式碼如下:

// 頁面底部TabItem上的圖示陣列
var tabImages;
  
// 頁面頂部的大標題(也是TabItem上的文字)
var appBarTitles = ['資訊', '動彈', '發現', '我的'];
  
// 資料初始化,包括TabIcon資料和頁面內容資料
void initData() {
  if (tabImages == null) {
    tabImages = [
      [
        getTabImage('images/ic_nav_news_normal.png'),
        getTabImage('images/ic_nav_news_actived.png')
      ],
      [
        getTabImage('images/ic_nav_tweet_normal.png'),
        getTabImage('images/ic_nav_tweet_actived.png')
      ],
      [
        getTabImage('images/ic_nav_discover_normal.png'),
        getTabImage('images/ic_nav_discover_actived.png')
      ],
      [
        getTabImage('images/ic_nav_my_normal.png'),
        getTabImage('images/ic_nav_my_pressed.png')
      ]
    ];
  }
}

// 傳入圖片路徑,返回一個Image元件
Image getTabImage(path) {
  return new Image.asset(path, width: 20.0, height: 20.0);
}
複製程式碼

上面的程式碼中需要注意的是Image元件,要使用image/目錄下的圖片,必須確保專案根目錄下的pubspec.yaml檔案中已經新增了圖片的路徑,如下圖:

從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建
如果沒有上面的assets配置,直接載入圖片是會報錯的。

為了達到點選Tab切換不同的頁面的功能,我們需要給CupertinoTabBar元件的onTap引數配置一個方法,該方法有一個index引數,我們將這個index賦值給前面定義的_tabIndex即可,並將這個賦值操作放到setState中執行,如下程式碼:

onTap: (index) {
  // 底部TabItem的點選事件處理,點選時改變當前選擇的Tab的索引值,則頁面會自動重新整理
  setState((){
    _tabIndex = index;
  });
},
複製程式碼

最後放上MyOSCClientState類的build方法程式碼:

@override
Widget build(BuildContext context) {
  initData();
  return new MaterialApp(
    theme: new ThemeData(
      // 設定頁面的主題色
      primaryColor: const Color(0xFF63CA6C)
    ),
    home: new Scaffold(
      appBar: new AppBar(
        // 設定AppBar標題
        title: new Text(appBarTitles[_tabIndex],
        // 設定AppBar上文字的樣式
        style: new TextStyle(color: Colors.white)),
        // 設定AppBar上圖示的樣式
        iconTheme: new IconThemeData(color: Colors.white)
      ),
      body: _body,
      // bottomNavigationBar屬性為頁面底部新增導航的Tab,CupertinoTabBar是Flutter提供的一個iOS風格的底部導航欄元件
      bottomNavigationBar: new CupertinoTabBar(
        items: getBottomNavItems(),
        currentIndex: _tabIndex,
        onTap: (index) {
          // 底部TabItem的點選事件處理,點選時改變當前選擇的Tab的索引值,則頁面會自動重新整理
          setState((){
            _tabIndex = index;
          });
        },
      )
    ),
  );
}
複製程式碼

在上面的程式碼中,body屬性是_body變數,而_body變數是個IndexedStack物件,IndexedStack物件的index值是_tabIndex,所以當我們在setState中改變了_tabIndex後,IndexedStack就會自動切換顯示子元件了,也就達到了切換頁面的目的。

上面的程式碼執行在模擬器中如下圖所示:

從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建 從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建 從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建

給首頁加上側滑選單

側滑選單在Flutter中已有相關元件,所以為首頁加上側滑選單的方法很簡單:給Scaffold元件傳個drawer引數即可,程式碼如下:

new Scaffold(
  appBar: new AppBar(
    // 設定AppBar標題
    title: new Text(appBarTitles[_tabIndex],
    // 設定AppBar上文字的樣式
    style: new TextStyle(color: Colors.white)),
    // 設定AppBar上圖示的樣式
    iconTheme: new IconThemeData(color: Colors.white)
  ),
  body: _body,
  // bottomNavigationBar屬性為頁面底部新增導航的Tab,CupertinoTabBar是Flutter提供的一個iOS風格的底部導航欄元件
  bottomNavigationBar: new CupertinoTabBar(
    items: getBottomNavItems(),
    currentIndex: _tabIndex,
    onTap: (index) {
      // 底部TabItem的點選事件處理,點選時改變當前選擇的Tab的索引值,則頁面會自動重新整理
      setState((){
        _tabIndex = index;
      });
    },
  ),
  // drawer屬性用於為當前頁面新增一個側滑選單
  drawer: new Drawer(
    child: new Center(
      child: new Text("this is a drawer")
    ),
  ),
)
複製程式碼

有了drawer之後的app執行效果如下圖:

從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建 從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建

實現頁面跳轉邏輯

在Flutter中實現頁面的跳轉非常簡單,使用Navigator的相關API即可,下面我們改造一下NewsListPage頁面,在頁面中間加入一個按鈕,點選按鈕跳轉到詳情頁。

首先我們在pages/目錄下新建一個NewsDetailPage代表資訊詳情頁,並新增如下程式碼:

import 'package:flutter/material.dart';

class NewsDetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("資訊詳情", style: new TextStyle(color: Colors.white)),
        iconTheme: new IconThemeData(color: Colors.white)
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text("News Detail Page."),
            new RaisedButton(
              child: new Text("Back"),
              onPressed: () {
                Navigator.of(context).pop();
              },
            )
          ],
        )
      ),
    );
  }
}
複製程式碼

上面的程式碼中有如下幾點需要注意:

  1. build方法中我們返回的是一個Scaffold元件,而不是像main.dart中那樣返回一個MaterialApp元件,這是因為我們在使用Navigator從資訊列表頁跳轉到詳情頁時,會自動為詳情頁的AppBar左邊新增返回按鈕,如果你在詳情頁還是使用MaterialApp物件,則頁面左上角不會自動新增返回按鈕。
  2. 上面程式碼中的body部分返回的是一個Center元件,Center中裝的是Column元件,如果你不為Column元件設定mainAxisAlignment: MainAxisAlignment.center,則頁面上的元件只會在水平方向居中而不會在垂直方向上居中。
  3. 使用Navigator.of(context).pop()來使頁面返回到上一級。

下面需要修改NewsListPage的程式碼,加入按鈕並完成跳轉到詳情頁的邏輯,程式碼如下:

import 'package:flutter/material.dart';
import 'NewsDetailPage.dart';

// 資訊列表頁面
class NewsListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Center(
      child: new RaisedButton(
        child: new Text("to detail page"),
        onPressed: () {
          Navigator.of(context).push(new MaterialPageRoute(builder: (ctx) {
            return new NewsDetailPage();
          }));
        }
      )
    );
  }
}
複製程式碼

使用Navigator.of(context).push()方法來完成頁面的跳轉,push的引數是一個Route物件,這裡使用了Flutter提供的MaterialPageRoute物件,builder引數是一個方法,返回的就是詳情頁物件。

除了使用上面的方式做頁面的跳轉外,還可以給MaterialApp配置一個route引數,該route引數類似於一個全域性的路由表,根據一個name值導航到對應的頁面,這種方式需要定義一個型別為Map<String, WidgetBuilder>的變數_route變數,並在initDate()方法中為這個變數賦值,如下程式碼:

_routes['newsDetail'] = (BuildContext) {
  return new NewsDetailPage();
};
複製程式碼

在需要跳轉頁面的時候,呼叫如下方法完成頁面跳轉:

Navigator.of(context).pushNamed("newsDetail");
複製程式碼

如果在頁面跳轉時需要給下一個頁面傳值,可以在下一個頁面的構造方法中接收傳入的值,然後在Navigator呼叫push方法的時候new下一個頁面時,在元件的構造方法中設定傳入的值,具體用法可以參考這裡

原始碼

本篇相關的所有原始碼都在GitHub上demo-flutter-osc專案的v0.1分支

後記

本篇主要記錄的是基於Flutter的開源中國客戶端整體佈局框架的搭建過程,通過使用Flutter內建的各種Widget,可以很容易的實現這個佈局框架,下一篇準備記錄的是基於Flutter的開源中國客戶端各個靜態頁面的實現。

我的開源專案

  1. 基於Google Flutter的開源中國客戶端,希望大家給個Star支援一下,原始碼:
從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建 從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建
  1. 基於Flutter的俄羅斯方塊小遊戲,希望大家給個Star支援一下,原始碼:
從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建 從0開始寫一個基於Flutter的開源中國客戶端(5)——App整體佈局框架搭建
上一篇 下一篇
從0開始寫一個基於Flutter的開源中國客戶端(4)
——Flutter佈局基礎
從0開始寫一個基於Flutter的開源中國客戶端(6)
——各個靜態頁面的實現

相關文章