这段时间太忙了,原计划两天更新一篇的计划也给耽误了,并且发现,计划四篇的文章三篇就够了,因此今天就来完成整个山寨项目。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上以代码的形式了,喜欢的小伙伴能够关注一下。源码点这里。