Flutter入門——山寨掘金(三)

MeFelixWang發表於2018-07-29

寫在前面

這段時間太忙了,原計劃兩天更新一篇的計劃也給耽誤了,而且發現,計劃四篇的文章三篇就夠了,所以今天就來完成整個山寨專案。

在前兩篇文章中,我們已經完成了底部tab中的首頁和發現頁,以及對應的一些頁面,今天我們先不做沸點頁和小冊頁,先做我的這一頁。

正式開始

一. 引入Redux

寫過 react 的小夥伴對 redux 一定不陌生,我們這裡引入 flutter_redux 這個外掛來管理登入狀態,它是國外的牛人寫的,小夥伴們之後自己瞭解吧,這裡為作者點個贊。

開啟 pubspec.yaml 寫入依賴,並 get 一下:

dependencies:
  flutter_redux: ^0.5.2

複製程式碼

然後開啟 main.dart ,引入 redux

import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
複製程式碼

接著,我們在 lib 下新建 reducers 資料夾,並在其中新建 reducers.dart ,寫入下列程式碼:

Map getUserInfo(Map userInfo, dynamic action) {
  if (action.type == 'SETUSERINFO') {
    userInfo = action.userInfo;
  } else if (action.type == 'GETUSERINFO') {}
  print(action.type);
  return userInfo;
}
複製程式碼

接著在 lib 下新建 actions 資料夾,並在其中新建 actions.dart ,寫入下列程式碼:

class UserInfo {
  String type;
  final Map userInfo;

  UserInfo(this.type,this.userInfo);
}

複製程式碼

小夥伴們一看就知道就是做獲取使用者資訊及修改使用者資訊的,就不多做解釋。

回到 main.dart ,引入 actionsreducers 並改造之前的程式碼:

import 'actions/actions.dart';
import 'reducers/reducers.dart';

void main() {
  final userInfo = new Store<Map>(getUserInfo, initialState: {});

  runApp(new MyApp(
    store: userInfo,
  ));
}

class MyApp extends StatelessWidget {
  final Store<Map> store;

  MyApp({Key key, this.store}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return new StoreProvider(
        store: store,
        child: new MaterialApp(
          home: new IndexPage(),
          theme: new ThemeData(
              highlightColor: Colors.transparent,
              //將點選高亮色設為透明
              splashColor: Colors.transparent,
              //將噴濺顏色設為透明
              bottomAppBarColor: new Color.fromRGBO(244, 245, 245, 1.0),
              //設定底部導航的背景色
              scaffoldBackgroundColor: new Color.fromRGBO(244, 245, 245, 1.0),
              //設定頁面背景顏色
              primaryIconTheme: new IconThemeData(color: Colors.blue),
              //主要icon樣式,如頭部返回icon按鈕
              indicatorColor: Colors.blue,
              //設定tab指示器顏色
              iconTheme: new IconThemeData(size: 18.0),
              //設定icon樣式
              primaryTextTheme: new TextTheme(
                  //設定文字樣式
                  title: new TextStyle(color: Colors.black, fontSize: 16.0))),
          routes: <String, WidgetBuilder>{
            '/search': (BuildContext context) => SearchPage(),
            '/activities': (BuildContext context) => ActivitiesPage(),
          },
        ));
  }
}

複製程式碼

我們用 StoreProvider 將根元件 MaterialApp 包裹起來,因為其他頁面都是在根元件下的,所以其他所有頁面都能獲取到 store 。到此我們就算是引入 redux 了。

二. 實現登入頁

我們這裡做的是使用者登入狀態的管理,所以我們先實現登入頁。

pages 下新建 signin.dart ,先引入所需要的東西:

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import '../actions/actions.dart';
import '../reducers/reducers.dart';
複製程式碼

接著,我們先定義一下變數啥的,後面會用到:

/*接著寫*/

class SignInPage extends StatefulWidget {
  @override
  SignInPageState createState() => new SignInPageState();
}

class SignInPageState extends State<SignInPage> {
  String account; //賬號
  String password; //密碼
  Map userInfo; //使用者資訊
  List signMethods = [ //其他登入方式
    'lib/assets/icon/weibo.png',
    'lib/assets/icon/wechat.png',
    'lib/assets/icon/github.png'
  ];
  RegExp phoneNumber = new RegExp(
      r"(0|86|17951)?(13[0-9]|15[0-35-9]|17[0678]|18[0-9]|14[57])[0-9]{8}"); //驗證手機正規表示式
  final TextEditingController accountController = new TextEditingController();
  final TextEditingController passwordController = new TextEditingController();

  //顯示提示資訊
  void showAlert(String value) {
    showDialog(
        context: context,
        builder: (context) {
          return new AlertDialog(
            content: new Text(value),
          );
        });
  }
}

複製程式碼

這裡只需注意兩個 controller ,因為我這裡用的是 TextField ,所以需要它們倆來對輸入框做一些控制。當然,小夥伴們也可以用 TextForm

class SignInPageState extends State<SignInPage> {
/*接著寫*/
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new Scaffold(
        appBar: new AppBar(
          backgroundColor: new Color.fromRGBO(244, 245, 245, 1.0),
          titleSpacing: 0.0,
          leading: new IconButton(
              icon: new Icon(Icons.chevron_left),
              onPressed: (() {
                Navigator.pop(context);
              })),
        ),
        body: new Container(
          child: new Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              new Container(
                child: new Column(
                  children: <Widget>[
                    new Container(
                        height: 80.0,
                        margin: new EdgeInsets.only(top: 30.0, bottom: 30.0),
                        child: new ClipRRect(
                          borderRadius: new BorderRadius.circular(15.0),
                          child: new Image.asset(
                            'lib/assets/img/juejin.jpg',
                          ),
                        )),
                    new Container(
                      decoration: new BoxDecoration(
                          border: new Border(
                              top: new BorderSide(
                                  width: 0.5, color: Colors.grey),
                              bottom: new BorderSide(
                                  width: 0.5, color: Colors.grey))),
                      margin: new EdgeInsets.only(bottom: 20.0),
                      child: new Column(
                        children: <Widget>[
                          new TextField(
                            decoration: new InputDecoration(
                                hintText: '郵箱/手機',
                                border: new UnderlineInputBorder(
                                    borderSide: new BorderSide(
                                        color: Colors.grey, width: 0.2)),
                                prefixIcon: new Padding(
                                    padding: new EdgeInsets.only(right: 20.0))),
                            controller: accountController,
                            onChanged: (String content) {
                              setState(() {
                                account = content;
                              });
                            },
                          ),
                          new TextField(
                            decoration: new InputDecoration(
                                border: InputBorder.none,
                                hintText: '密碼',
                                prefixIcon: new Padding(
                                    padding: new EdgeInsets.only(right: 20.0))),
                            controller: passwordController,
                            onChanged: (String content) {
                              setState(() {
                                password = content;
                              });
                            },
                          ),
                        ],
                      ),
                    ),
                    new Container(
                        padding: new EdgeInsets.only(left: 20.0, right: 20.0),
                        child: new Column(
                          children: <Widget>[
                            new StoreConnector<Map, VoidCallback>(
                              converter: (store) {
                                return () => store.dispatch(
                                    UserInfo('SETUSERINFO', userInfo));
                              },
                              builder: (context, callback) {
                                return new Card(
                                  color: Colors.blue,
                                  child: new FlatButton(
                                      onPressed: () {
                                        if (account == null) {
                                          showAlert('請輸入賬號');
                                        } else if (password == null) {
                                          showAlert('請輸入密碼');
                                        } else if (phoneNumber
                                            .hasMatch(account)) {
                                          String url =
                                              "https://juejin.im/auth/type/phoneNumber";
                                          http.post(url, body: {
                                            "phoneNumber": account,
                                            "password": password
                                          }).then((response) {
                                            if (response.statusCode == 200) {
                                              userInfo =
                                                  json.decode(response.body);
                                              callback();
                                              Navigator.pop(context);
                                            }
                                          });
                                        } else {
                                          showAlert('請輸入正確的手機號碼');
                                        }
                                      },
                                      child: new Row(
                                        mainAxisAlignment:
                                            MainAxisAlignment.center,
                                        children: <Widget>[
                                          new Text(
                                            '登入',
                                            style: new TextStyle(
                                                color: Colors.white),
                                          )
                                        ],
                                      )),
                                );
                              },
                            ),
                            new Row(
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
                              children: <Widget>[
                                new FlatButton(
                                  onPressed: () {},
                                  child: new Text(
                                    '忘記密碼?',
                                    style: new TextStyle(color: Colors.grey),
                                  ),
                                ),
                                new FlatButton(
                                    onPressed: () {},
                                    child: new Text(
                                      '註冊賬號',
                                      style: new TextStyle(color: Colors.blue),
                                    )),
                              ],
                            )
                          ],
                        )),
                  ],
                ),
              ),
              new Container(
                child: new Column(
                  children: <Widget>[
                    new Text('其他登入方式'),
                    new Row(
                        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                        children: signMethods.map((item) {
                          return new IconButton(
                              icon: new Image.asset(
                                item,
                                color: Colors.blue,
                              ),
                              onPressed: null);
                        }).toList()),
                    new Text(
                      '掘金 · juejin.im',
                      style: new TextStyle(
                        color: Colors.grey,
                        fontSize: 12.0,
                      ),
                    ),
                    new Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        new Icon(
                          Icons.check_circle,
                          color: Colors.grey,
                          size: 14.0,
                        ),
                        new Text(
                          '已閱讀並同意',
                          style:
                              new TextStyle(color: Colors.grey, fontSize: 12.0),
                        ),
                        new FlatButton(
                            onPressed: null,
                            child: new Text(
                              '軟體許可服務協議',
                              style: new TextStyle(
                                  decoration: TextDecoration.underline,
                                  decorationColor: const Color(0xff000000),
                                  fontSize: 12.0),
                            ))
                      ],
                    )
                  ],
                ),
              )
            ],
          ),
        ));
  }
}

複製程式碼

頁面長這個樣子:

Flutter入門——山寨掘金(三)

這部分內容稍微有點複雜,巢狀也比較多,我說一下關鍵點。 首先是 Image.asset ,這個元件是用來從我們的專案中引入圖片,但使用前需要寫入依賴。在 lib 下新建一個資料夾用於存放圖片:

Flutter入門——山寨掘金(三)

然後到 pubspec.yaml 下寫依賴:

Flutter入門——山寨掘金(三)

這樣才能使用。

其次是在需要和 store 通訊的地方用 StoreConnector 將元件包裹起來,我們這裡主要是下面這一段:

  new StoreConnector<Map, VoidCallback>(
                               converter: (store) {
                                 return () => store.dispatch(
                                     UserInfo('SETUSERINFO', userInfo));
                               },
                               builder: (context, callback) {
                                 return new Card(
                                   color: Colors.blue,
                                   child: new FlatButton(
                                       onPressed: () {
                                         if (account == null) {
                                           showAlert('請輸入賬號');
                                         } else if (password == null) {
                                           showAlert('請輸入密碼');
                                         } else if (phoneNumber
                                             .hasMatch(account)) {
                                           String url =
                                               "https://juejin.im/auth/type/phoneNumber";
                                           http.post(url, body: {
                                             "phoneNumber": account,
                                             "password": password
                                           }).then((response) {
                                             if (response.statusCode == 200) {
                                               userInfo =
                                                   json.decode(response.body);
                                               callback();
                                               Navigator.pop(context);
                                             }
                                           });
                                         } else {
                                           showAlert('請輸入正確的手機號碼');
                                         }
                                       },
                                       child: new Row(
                                         mainAxisAlignment:
                                             MainAxisAlignment.center,
                                         children: <Widget>[
                                           new Text(
                                             '登入',
                                             style: new TextStyle(
                                                 color: Colors.white),
                                           )
                                         ],
                                       )),
                                 );
                               },
                             ),
複製程式碼

converter 返回一個函式,內容就是對 store 進行的操作,我們這裡是登入,需要把登入資訊寫入 store ,所以這裡是 SETUSERINFO 。這個返回的函式會被 builder 作為第二個引數,我們在呼叫掘金介面並登入成功後呼叫此函式將登入資訊寫入 store 。我這裡做的是登入成功後回到之前的頁面。

我們回到 main.dart ,新增一下路由:

import 'pages/signin.dart';
/*略過*/
  routes: <String, WidgetBuilder>{
            '/search': (BuildContext context) => SearchPage(),
            '/activities': (BuildContext context) => ActivitiesPage(),
            '/signin': (BuildContext context) => SignInPage(),
          },
複製程式碼

其實頁面寫完,登入功能也就可以用了,但是我們得有一個入口進入到登入頁面,所以我們接下來實現我的頁面。

三. 實現我的頁面

開啟 mine.dart ,先引入需要的東西並定義一些變數:

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import '../actions/actions.dart';
import '../reducers/reducers.dart';

class MinePage extends StatefulWidget {
  @override
  MinePageState createState() => new MinePageState();
}

class MinePageState extends State<MinePage> {
  List infoList = [
    {
      'key': 'msgCenter',
      'content': {
        'title': '訊息中心',
        'icon': Icons.notifications,
        'color': Colors.blue,
        'path': '/msgCenter'
      }
    },
    {
      'key': 'collectedEntriesCount',
      'content': {
        'title': '我喜歡的',
        'icon': Icons.favorite,
        'color': Colors.green,
        'path': '/like'
      }
    },
    {
      'key': 'collectionSetCount',
      'content': {
        'title': '收藏集',
        'icon': Icons.collections,
        'color': Colors.blue,
        'path': '/collections'
      }
    },
    {
      'key': 'postedEntriesCount',
      'content': {
        'title': '已購小冊',
        'icon': Icons.shop,
        'color': Colors.orange,
        'path': '/myBooks'
      }
    },
    {
      'key': 'collectionSetCount',
      'content': {
        'title': '我的錢包',
        'icon': Icons.account_balance_wallet,
        'color': Colors.blue,
        'path': '/myWallet'
      }
    },
    {
      'key': 'likedPinCount',
      'content': {
        'title': '贊過的沸點',
        'icon': Icons.thumb_up,
        'color': Colors.green,
        'path': '/pined'
      }
    },
    {
      'key': 'viewedEntriesCount',
      'content': {
        'title': '閱讀過的文章',
        'icon': Icons.remove_red_eye,
        'color': Colors.grey,
        'path': '/read'
      }
    },
    {
      'key': 'subscribedTagsCount',
      'content': {
        'title': '標籤管理',
        'icon': Icons.picture_in_picture,
        'color': Colors.grey,
        'path': '/tags'
      }
    },
  ];
}

複製程式碼

這裡的 infoList 就是一些選項,提出來寫是為了讓整體程式碼看著舒服點。路由我也寫在裡面了,等之後有空再慢慢完善吧。接著:

class MinePageState extends State<MinePage> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new StoreConnector<Map, Map>(
        converter: (store) => store.state,
        builder: (context, info) {
          Map userInfo = info;
          if (userInfo.isNotEmpty) {
            infoList.map((item) {
              item['content']['count'] = userInfo['user'][item['key']];
            }).toList();
          }
          return new Scaffold(
            appBar: new AppBar(
              title: new Text('我'),
              centerTitle: true,
              backgroundColor: new Color.fromRGBO(244, 245, 245, 1.0),
            ),
            body: new ListView(
              children: <Widget>[
                new StoreConnector<Map, Map>(
                  converter: (store) => store.state,
                  builder: (context, info) {
                    if(info.isEmpty){}else{}
                    return new Container(
                      child: new ListTile(
                        leading: info.isEmpty?
                          new CircleAvatar(
                          child: new Icon(Icons.person, color: Colors.white),
                          backgroundColor: Colors.grey,
                        ):new CircleAvatar(backgroundImage: new NetworkImage(info['user']['avatarLarge']),),
                        title: info.isEmpty
                            ? new Text('登入/註冊')
                            : new Text(info['user']['username']),
                        subtitle: info.isEmpty
                            ? new Container(
                                width: 0.0,
                                height: 0.0,
                              )
                            : new Text(
                                '${info['user']['jobTitle']} @ ${info['user']['company']}'),
                        enabled: true,
                        trailing: new Icon(Icons.keyboard_arrow_right),
                        onTap: () {
                          Navigator.pushNamed(context, '/signin');
                        },
                      ),
                      padding: new EdgeInsets.only(top: 15.0, bottom: 15.0),
                      margin: const EdgeInsets.only(top: 15.0, bottom: 15.0),
                      decoration: const BoxDecoration(
                          border: const Border(
                            top: const BorderSide(
                                width: 0.2,
                                color:
                                    const Color.fromRGBO(215, 217, 220, 1.0)),
                            bottom: const BorderSide(
                                width: 0.2,
                                color:
                                    const Color.fromRGBO(215, 217, 220, 1.0)),
                          ),
                          color: Colors.white),
                    );
                  },
                ),
                new Column(
                    children: infoList.map((item) {
                  Map itemInfo = item['content'];
                  return new Container(
                    decoration: new BoxDecoration(
                        color: Colors.white,
                        border: new Border(bottom: new BorderSide(width: 0.2))),
                    child: new ListTile(
                      leading: new Icon(
                        itemInfo['icon'],
                        color: itemInfo['color'],
                      ),
                      title: new Text(itemInfo['title']),
                      trailing: itemInfo['count'] == null
                          ? new Container(
                              width: 0.0,
                              height: 0.0,
                            )
                          : new Text(itemInfo['count'].toString()),
                      onTap: () {
                        Navigator.pushNamed(context, itemInfo['path']);
                      },
                    ),
                  );
                }).toList()),
                new Column(
                  children: <Widget>[
                    new Container(
                      margin: new EdgeInsets.only(top: 15.0),
                      decoration: new BoxDecoration(
                          color: Colors.white,
                          border: new Border(
                              top: new BorderSide(width: 0.2),
                              bottom: new BorderSide(width: 0.2))),
                      child: new ListTile(
                        leading: new Icon(Icons.insert_drive_file),
                        title: new Text('意見反饋'),
                      ),
                    ),
                    new Container(
                      margin: new EdgeInsets.only(bottom: 15.0),
                      decoration: new BoxDecoration(
                          color: Colors.white,
                          border:
                              new Border(bottom: new BorderSide(width: 0.2))),
                      child: new ListTile(
                        leading: new Icon(Icons.settings),
                        title: new Text('設定'),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          );
        });
  }
}
複製程式碼

這裡也是一樣,因為我們整個頁面都會用到 store ,所以我們在最外層使用 StoreConnector ,程式碼中有很多三元表示式,這個是為了在是否有登陸資訊兩種狀態下顯示不同內容的,完成後的頁面長這個樣子:

Flutter入門——山寨掘金(三)

為什麼顯示的是登入/註冊呢?因為我們沒登入啊,哈哈!放一張完成後的聯動圖:

Flutter入門——山寨掘金(三)

小夥伴們可以看到,登入後會顯示使用者的一些資訊,細心的小夥伴會發現輸入賬號密碼的時候會提示超出了,我個人覺得這個應該是正常的吧,畢竟底部鍵盤彈起來肯定會遮擋部分頁面。其他需要用到登入狀態的地方也是一樣的寫法。

結尾叨叨

至此,此入門教程就完結了。由於文章篇幅,沸點和小冊兩個tab頁面我就不貼了,相信如果是從第一篇文章看到現在的小夥伴都會寫了。

總結一下我們學習的東西,主要涉及的知識點如下:

  • 基礎元件的使用
  • 網路請求
  • 路由配置及傳參
  • html 程式碼的渲染
  • 使用 redux 做狀態管理

總結完了感覺沒多少東西,不過我也是初學者,水平有限,文中的不足及錯誤還請指出,一起學習、交流。之後的話專案我會不時更新,不過是在GitHub上以程式碼的形式了,喜歡的小夥伴可以關注一下。原始碼點這裡

相關文章