這段時間太忙了,原計劃兩天更新一篇的計劃也給耽誤了,並且發現,計劃四篇的文章三篇就夠了,因此今天就來完成整個山寨項目。html
在前兩篇文章中,咱們已經完成了底部tab中的首頁和發現頁,以及對應的一些頁面,今天咱們先不作沸點頁和小冊頁,先作個人這一頁。react
寫過 react
的小夥伴對 redux
必定不陌生,咱們這裏引入 flutter_redux
這個插件來管理登陸狀態,它是國外的牛人寫的,小夥伴們以後本身瞭解吧,這裏爲做者點個贊。git
打開 pubspec.yaml
寫入依賴,並 get
一下:github
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
,寫入下列代碼:json
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
,寫入下列代碼:redux
class UserInfo {
String type;
final Map userInfo;
UserInfo(this.type,this.userInfo);
}
複製代碼
小夥伴們一看就知道就是作獲取用戶信息及修改用戶信息的,就很少作解釋。網絡
回到 main.dart
,引入 actions
和 reducers
並改造以前的代碼:app
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
了。less
咱們這裏作的是用戶登陸狀態的管理,因此咱們先實現登陸頁。
在 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上以代碼的形式了,喜歡的小夥伴能夠關注一下。源碼點這裏。