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

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

App總體佈局框架搭建

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

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

新建項目

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

使用MaterialApp和Scaffold組件構建首頁

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

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代碼:less

// 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組件的文本。ide

在上一步中,咱們的Scaffold組件裏的body屬性只是一個Text組件,爲了加載上面的4個頁面,須要用一個容器組件將這4個頁面裝起來,而後在點擊Tab時切換頁面,這就用到了我以前的博文裏說到的IndexedStack組件了。IndexedStack中能夠有多個子組件,根據索引值來顯示其中某個組件而隱藏其他的組件。佈局

在第一步中MyOSCClientState類中定義兩個變量:_tabIndex_body_tabIndex表示當前頁面底部選中的Tab的索引,_body表示首頁Scaffold組件的body屬性值,而後給_tabIndex_body變量賦值,以下代碼所示:post

// 頁面當前選中的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文件中已經添加了圖片的路徑,以下圖:

若是沒有上面的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就會自動切換顯示子組件了,也就達到了切換頁面的目的。

上面的代碼運行在模擬器中以下圖所示:

給首頁加上側滑菜單

側滑菜單在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運行效果以下圖:

實現頁面跳轉邏輯

在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支持一下,源碼:

  2. 基於Flutter的俄羅斯方塊小遊戲,但願你們給個Star支持一下,源碼:

相關文章
相關標籤/搜索