Flutter News App

透過 flutter 建置新聞 app

Gary Ng
16 min readNov 14, 2020

本章節透過 flutter 撰寫簡單的 news app, 他將會包含了基本的登入以及註冊以及新聞清單等。

github 的 master branch

github 專案: https://github.com/xingobar/flutter-news.git

  1. 首先沒有安裝環境的可以透過官網的教學進行安裝

2. 透過指定先創建專案

flutter create flutter_news

3. 將 lib/main.dart 程式碼進行修改

return MaterialApp(    title: 'News',    debugShowCheckedModeBanner: false, // 不顯示測試的 debug mode    theme: ThemeData(       brightness: Brightness.dark, // 亮度       primarySwatch: Colors.blue,       textTheme: TextTheme( // 字型樣式         headline: TextStyle(fontSize: 20.0),     )),   home: Scaffold(     body: SafeArea(       child: Container(        child: Text('test'),        ),       ),    ),  );}

4. 在 lib 新增一個 pages 資料夾, 然後在 pages 資料夾下新增 login.dart

5. login.dart 程式碼架構

6. 在 lib 新建 res 資料夾且在裡面新增 string.dart 以及 index.dart

string.dart 輸入一些文字的變數如下

class Ids {    static const LOGIN_ACCOUNT_LABEL = '帳號';    static const LOGIN_ACCOUNT_HINT = '請輸入帳號';    static const LOGIN_PASSWORD_LABEL = '密碼';    static const LOGIN_PASSWORD_HINT = '請輸入密碼';}

index.dart

export 'string.dart'

ps: 這樣只要之後 import res 的 index.dart 的話, 就可以使用 Ids 變數裡面的名稱

然後 Login 因為是 state 會改動的, 所以要使用 StatefulWidget

首先在 _LoginState 宣告

// 帳號 focusfinal _accountFocusNode = new FocusNode();// 密碼 focusfinal _passwordFocusNode = new FocusNode();// 帳號 controllerTextEditingController _accountController = new TextEditingController();// 密碼 controllerTextEditingController _passwordController = new TextEditingController();

接著設定帳號輸入匡

TextField(   focusNode: _accountFocusNode, // 設定 focus   controller: _accountController, // 設定 controller   decoration: InputDecoration(   // 設定 input 樣式       border: OutlineInputBorder(),       labelText: Ids.LOGIN_ACCOUNT_LABEL, // 使用 string.dart        hintText: Ids.LOGIN_ACCOUNT_HINT), //  使用 string.dart       onSubmitted: (String value) {        print('trigger event');       },)

密碼的輸入匡也跟帳號雷同, 只是多了 obscureText: true

obscureText: true

結果:

接著製作登入按鈕, 註冊按鈕也雷同

ButtonTheme(    minWidth: _getInputWidth(), /// 設定按鈕的最小寬度, 也可以使用 SizeBox 設定    height: Dimens.height_dp_50, // 在 res 裡面新增 dimens.dart    child: RaisedButton(    shape: RoundedRectangleBorder( // 設定圓形的 border        borderRadius:          BorderRadius.circular(Dimens.border_radius_50)),        color: Colors.white30,        child: Text(‘登入’),           onPressed: () {             print(‘login’);       },    ),)

結果如下:

接著會發現程式很多重複的地方, 我們來調整一下

創建 _buildTextField 函式, 因為大家的 text field 樣式都雷同

// 創建 text fieldTextField _buildTextField(     FocusNode focusNode,     TextEditingController inputController,     String labelText,     String hintText,     Function callback) {       return TextField(                 focusNode: focusNode,                 controller: inputController,                 decoration: InputDecoration(                   border: OutlineInputBorder(),                   labelText: labelText,                   hintText: hintText,                      ),                onSubmitted: (String value) {                   // callback                  callback(value);                },           );}

建立 _buildButton 函式

// 登入Widget _buildButton(String title, Color color, Function callback) {     return ButtonTheme(           minWidth: _getInputWidth(),          height: Dimens.height_dp_50,          child: RaisedButton(             shape: RoundedRectangleBorder(             borderRadius: BorderRadius.circular(Dimens.border_radius_50)),             color: color,             child: Text(title),                onPressed: () {                    callback();             },       ),     );}

新增帳號以及密碼的驗證函示, 以下先使用最簡單的判斷, 判斷帳號密碼是否是所規定的, 倘若不是的話則會顯示錯誤, 正在之後會進行調整

// 檢查帳號有效性void checkAccountValid() {    if (_accountController.text != Ids.TEST_ACCOUNT) {        setState(() {           _accountError = '帳號錯誤';        });;       _accountFocusNode.requestFocus(); // 設置焦點     }}// 檢查密碼有效性void checkPasswordValid() {    if (_passwordController.text != Ids.TEST_PASSWORD) {       setState(() {        _passwordError = '密碼錯誤';       });      _passwordFocusNode.requestFocus(); // 設置焦點   }}

完整畫面如下:

以上這樣登入畫面就建置好了

接著我們已同樣的方式建置註冊畫面

首先我們先新增一個 register.dart, 而註冊畫面跟登入畫面一樣是有狀態性的, 所以我們將其設置為 stateful 的 widget 如下代碼:

註冊這邊會使用一個新的 widget 叫做 SingleChildScrollView

這個元件是為了避免因為鍵盤的出現導致畫面繪製的時候出錯, 比較建議使用的場景為元件數較少的情況下使用, 倘若資料會是動態繪製出來的話, 建議改為使用 ListView

SingleChildScrollView(
child: Column(
children: <Widget> [
// 建置輸入帳號匡

// 建置輸入密碼匡
// 建置輸入確認密碼匡 // 導回登入畫面的 ] ))

以上為要在 SingleChildScrollView 擺放的 widget

在 string.dart 增加以下 const variable

// 註冊static const REGISTER_ACCOUNT_LABEL = '帳號';static const REGISTER_ACCOUNT_HINT = '請輸入帳號';static const REGISTER_PASSWORD_LABEL = '密碼';static const REGISTER_PASSWORD_HINT = '請輸入密碼';static const REGISTER_CONFIRM_PASSWORD_LABEL = '輸入密碼';static const REGISTER_CONFIRM_PASSWORD_HINT = '請再次輸入密碼';

建置輸入帳號的輸入匡, 而密碼以及確認密碼程式碼都類似

Container(    width: this._getInputWidth(),    child: TextField(          focusNode: _accountFocusNode,          controller: _accountController,          decoration: InputDecoration(             hintText: Ids.REGISTER_ACCOUNT_HINT,             labelText: Ids.REGISTER_ACCOUNT_LABEL,             border: OutlineInputBorder(                  borderRadius: BorderRadius.circular(Dimens.border_radius_50),           )         ),     ),)

增加帳號以及密碼的驗證

帳號以及密碼的驗證雷同

// 確認帳號是否驗證通過Map<String,String> _checkAccountValid() {    Map<String, String> errorMessage = {}; // 儲存錯誤訊息    if (_accountController.text.length < 6) {       errorMessage['Error'] = Error.ERROR_ACCOUNT_MIN;    }    return errorMessage;}

新增一個 _handleAuth 函式, 而 handleAuth 跟前面的登入程式略顯不同

void _handleAuth() {    Map<String, String> error = {}; // 用於存放錯誤訊息    error = this._checkAccountValid();    if (error.keys.length != 0) {       _accountError = error['Error'];       _accountFocusNode.requestFocus();    } else {       _accountError = null;    }   error = this._checkPasswordValid();     if (error.keys.length != 0) {      _passwordError = error['Error'];      _passwordFocusNode.requestFocus();     } else {    _passwordError = null;   }    error = this._checkConfirmPasswordValid();    if (error.keys.length != 0) {       _confirmPasswordError = error['Error'];       _confirmPasswordFocusNode.requestFocus();   } else {     _confirmPasswordError = null;   }  setState(() {     _accountError =_accountError;     _passwordError = _passwordError;     _confirmPasswordError =_confirmPasswordError;   });}

註冊畫面

而我們一開始跳轉的方式是使用

Navigator.of(context).push(context, MaterialPageRouter(  builder: (context) => new Login() or new Register()
))

而現在我們改為替 router 命名

在 lib 資料夾下多新增 router.dart

在 main.dart 要設置 onGenerateRoute

這樣我們在頁面就可以使用匿名過的路由了

如下所示:

現在我們來製作首頁

首頁只會單純顯示資料所以是使用 StatelessWidget

在首頁我們先使用 Drawer 製作側邊欄, 只是如下範例將 Drawer 抽成一個 Widget, 供其他頁面使用, 因為代碼有點長, 所以這邊只顯示片段

而要如何顯示 drawer 呢, 要在 scaffold 增加 drawer 的參數, 如下圖

結果會如下:

接著開始製作首頁內容, 我們會使用 listview 建置內容, 並且透過 ListView 搭配 Container 以及 Card, 以利於製作卡片顯示

itemCount: 跑幾次 listview

itemBuilder: 卡片顯示的內容

結果如下:

安裝 fontawesome 套件, 在 pubspec.yaml 增加以下數值

font_awesome_flutter: ^8.10.0

接著創建首頁的 Bottom 導覽列

在 ui 資料夾下新增 BottomNavigationWidget class, 因為要記錄點擊的索引, 所以該 widget 是要 StatefulWidget, 程式如下圖

底部導航結果

首頁增加 tabbar, 如下圖

首先先將 home.dart 的結構調整, 將最外層改為 DefaultTabController 如下圖

在使用 tabController 的同時需要宣告 _tabController

tabList 的變數位於 string.dart, 如下圖

我們接著為首頁增加輪播圖, 首先我們先來安裝 carousel_slider 套件, 先在 pubspec.yaml 增加

根據文件進行備至 CarouselController

這邊讓輪播圖自動播放, 且每三秒換下一張, 然後總數為 5 個以及設置初始值

設置輪播圖的小圓點

增加滑到底的時候會有 loading 圖示去增加 card

要將原先的 NewListWidget 進行調整, 因為要設置 SingleScrollView,

而我們在外面設置 Column , 因為要設置 Expand, 而要設置 Expand 的前提是他的 parent 需要是 Flex。不過我們首先先設置 scroll 監聽的狀態

ListView 我們要先關閉 scroll 事件, 否則會跟 SingleChildScrollView 衝突

多增加是否顯示加載圖示

接著要製作影音畫面

要先將 BottomNavigationWidget 的 constructor 進行調整, 我們讓他可以接受索引值, 指定要亮哪個顏色的 item, 否則因為我們換頁的時候是使用 Navigation pushNamed, 資料會被蓋掉

上面範例的左邊圖片是抓取 youtube 影片的縮圖, 要抓取我們先安裝以下套件

引入套件

接著我們宣告取得影片縮圖的函示

而清單的部份我們使用 ListView 進行繪製, 如下圖

建置爆料畫面

首先在 pages 新增一個 talk.dart , 設置為 StatefulWidget, 且宣告以下 controller

建置 label text 的 function

以下為部分代碼

新增照片功能, 首先要先安裝 image_picker 套件

匯入套件以及 dart:io

--

--

Gary Ng
Gary Ng

Written by Gary Ng

軟體工程師、後端工程師

No responses yet