寫在前面
這段時間太忙了,原計劃兩天更新一篇的計劃也給耽誤了,而且發現,計劃四篇的文章三篇就夠了,所以今天就來完成整個山寨專案。
在前兩篇文章中,我們已經完成了底部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
,引入 actions
和 reducers
並改造之前的程式碼:
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),
))
],
)
],
),
)
],
),
));
}
}
複製程式碼
頁面長這個樣子:
這部分內容稍微有點複雜,巢狀也比較多,我說一下關鍵點。
首先是 Image.asset
,這個元件是用來從我們的專案中引入圖片,但使用前需要寫入依賴。在 lib
下新建一個資料夾用於存放圖片:
然後到 pubspec.yaml
下寫依賴:
這樣才能使用。
其次是在需要和 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
,程式碼中有很多三元表示式,這個是為了在是否有登陸資訊兩種狀態下顯示不同內容的,完成後的頁面長這個樣子:
為什麼顯示的是登入/註冊呢?因為我們沒登入啊,哈哈!放一張完成後的聯動圖:
小夥伴們可以看到,登入後會顯示使用者的一些資訊,細心的小夥伴會發現輸入賬號密碼的時候會提示超出了,我個人覺得這個應該是正常的吧,畢竟底部鍵盤彈起來肯定會遮擋部分頁面。其他需要用到登入狀態的地方也是一樣的寫法。
結尾叨叨
至此,此入門教程就完結了。由於文章篇幅,沸點和小冊兩個tab頁面我就不貼了,相信如果是從第一篇文章看到現在的小夥伴都會寫了。
總結一下我們學習的東西,主要涉及的知識點如下:
- 基礎元件的使用
- 網路請求
- 路由配置及傳參
html
程式碼的渲染- 使用
redux
做狀態管理
總結完了感覺沒多少東西,不過我也是初學者,水平有限,文中的不足及錯誤還請指出,一起學習、交流。之後的話專案我會不時更新,不過是在GitHub上以程式碼的形式了,喜歡的小夥伴可以關注一下。原始碼點這裡。