本文主要针对有Dart和Flutter基础的小伙伴,Dart和Flutter基础,废话不多说,开撸。
文件配置
1.打开终端,cd到你想创建的根目录,然后执行flutter create xxxx
(xxx为项目名),如显示如下图,即项目创建成功。
2.这里我使用的VSCode,用VSCode打开创建的项目,在lib目录下创建config、pages、tools文件夹
- config 用来配置网络请求以及路由文件
- pages 根据需求自己定义的页面以及组件
- tools 存放自己封装的工具类
路由配置
在config文件夹下创建route.dart
,在route.dart
中配置路由
import 'package:flutter/material.dart';
final routes = {
};
// ignore: strong_mode_top_level_function_literal_block
var onGenerateRoute = (RouteSettings settings) {
final String name = settings.name;
final Function pageContentBuilder = routes[name];
// print(name);
if (pageContentBuilder != null) {
if (settings.arguments != null) {
final Route route = MaterialPageRoute(
builder: (context) =>
pageContentBuilder(context, arguments: settings.arguments));
return route;
} else {
final Route route =
MaterialPageRoute(builder: (context) => pageContentBuilder(context));
return route;
}
}
};
-
routes
在这个Map对象中配置创建好的页面 -
onGenerateRoute
这个方法是谷歌官方为了配置路由为我们提供的,所以直接复制就可以了。有兴趣的小伙伴可以研究下。
工具代码封装
Flutter为我们提供了很多优秀的第三方库,这里我们使用的第三方网络库是Dio
,当然也有很多其他的库如http
,小伙伴可以https://pub.flutter-io.cn/查找
网络工具
1.在pubspec.yaml
的依赖中导入Dio
2.在config文件夹中创建
service_methon.dart
,在service_methon.dart
中对Dio按我们自己的需要做一点小处理
import 'package:dio/dio.dart';
import 'dart:async';
import 'dart:io';
Future cyw_getNetworkData(String type, String url, {Map dataDic}) async {
Response res;
if (type == 'POST') {
try {
res = await Dio().post(url, data: dataDic);
return res;
} catch (e) {
print(e);
}
} else if (type == 'GET') {
try {
res = await Dio().get(url);
return res;
} catch (e) {
print(e);
}
}
}
-
type
网络请求类型 -
url
网络请求地址 -
{Map dataDic}
网络请求参数(可选参数) -
Future
是在未来某个时间获得想要对象的一种手段。简单来说,就是我们能够通过它在某个时间点获得异步任务中返回的值。实际上,就是给 Future 设置回调函数,当异步任务执行完成后,会调用回调函数。
cyw_getNetworkData
此方法是一个异步的网络请求方法,在Dart中方法名后要加上async
关键字
3.config文件夹中创建service_url.dart
,在service_url.dart
配置URL
md5加密类
tools文件中创建encrypt.dart
,Dart
中直接为我们提供了MD5加密,导入相关库就可以直接使用了
import 'dart:convert';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
// md5 加密
String generateMd5(String data) {
var content = new Utf8Encoder().convert(data);
var digest = md5.convert(content);
// 这里其实就是 digest.toString()
return hex.encode(digest.bytes);
}
本地图片配置
1.准备本地图片,在根目录下创建images文件夹,并准备2x,3x文件夹(高清图准备),在images文件中存放图片
2.在
pubspec.yaml
-assets
中配置本地图片
assets:
- images/logo.png
- images/back.png
- images/personal.png
- images/home_bg.png
- images/ai-i.png
- images/bangzhu.png
- images/chakanmingxi.png
- images/jilu.png
- images/qianbao.png
- images/yijianfankui.png
- images/pic_head.png
- images/ic_right.png
个人觉得Flutter本地图片配置相对于iOS有点憨,不知道以后会不会有改进。
以上基本准备工作就完成了,接下来步入正题。
登录注册界面
登录首页界面
效果图11.在tools
文件夹下创建iosTypeButton.dart
,并封装此按钮组件,个人喜欢iOS风格,所以就创建了cupertino
风格的,代码如下:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class iosTypeBtn extends StatelessWidget {
double height;
double width;
String name;
Color bgColor;
Color textColor;
double fontSize = 14;
double radius = 0;
Function onPressed;
iosTypeBtn(this.width, this.height, this.name, this.onPressed,
{this.bgColor, this.radius, this.fontSize, this.textColor});
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
width: this.width,
height: this.height,
child: CupertinoButton(
child: Text(
this.name,
style: TextStyle(color: this.textColor, fontSize: this.fontSize),
),
color: this.bgColor,
onPressed: this.onPressed,
borderRadius: BorderRadius.all(Radius.circular(this.radius)),
),
);
}
}
2.进入main.dart
,把Flutter自动生成的代码删除,按照如下方式配置路由和界面
import 'package:flutter/material.dart';
import 'config/route.dart';
void main(){
return runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,//是否显示debug banner
initialRoute: '/loginMain',//第一次加载显示的路由
routes:routes,//配置的路由
onGenerateRoute: onGenerateRoute, //传入google固定的函数
);
}
}
3.创建登录页面的首页界面,即loginMain
页面。在pages文件夹中创建loginMianPage.dart
,并导入之前封装好的按钮组件,代码如下:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../tools/iosTypeButton.dart';
class LoginMainPage extends StatelessWidget {
const LoginMainPage({Key key}) : super(key: key);
// logo展示 自定义方法google建议带下划线
Widget _logo(context) {
return Container(
width: MediaQuery.of(context).size.width,
child: Image.asset('images/logo.png'),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.white,
child: Column(
children: <Widget>[
_logo(context),
iosTypeBtn(360, 60, '用户登录', () {
// Navigator.of(context).pushNamed('/loginPage');
print('用户登录');
},
textColor: Colors.white,
bgColor: Color.fromRGBO(58, 184, 228, 1),
fontSize: 16,
radius: 0),
SizedBox(
height: 20,
),
iosTypeBtn(360, 60, '用户注册', () {
// Navigator.of(context).pushNamed('/registPage');
print('用户注册');
},
textColor: Color.fromRGBO(102, 102, 102, 1),
bgColor: Color.fromRGBO(243, 243, 243, 1),
fontSize: 16,
radius: 0),
],
),
),
);
}
}
4.点击事件路由配置。在pages
文件夹下面创建loginPage.dart
和registPage.dart
,然后在config
-route.dart
配置需要跳转的路由:
import 'package:flutter/material.dart';
import '../pages/loginMainPage.dart';
import '../pages/loginPage.dart';
import '../pages/registPage.dart';
final routes = {
'/loginMain':(context) => LoginMainPage(),
'/loginPage':(context) => LoginPage(),
'/registPage':(context) => RegistPage(),
};
最后将loginMianPage
中注释的 Navigator.of(context).pushNamed('/registPage')
和Navigator.of(context).pushNamed('/loginPage')
打开就可以实现页面的跳转了。
至此,此界面功能基本完成。
登录界面
效果图21.此界面会用到2个第三库:
shared_preferences用来存储用户登录信息,类似iOS中NSUserDefaults
fluttertoast 一个轻量化的Toast弹窗
按官方文档集成即可。
2.界面代码如下:
// 返回按钮
Widget _backBtn(context) {
return Container(
height: 24.0,
width: 24.0,
child: InkWell(
child: Image.asset('images/back.png'),
onTap: () {
Navigator.of(context).pop();
},
),
);
}
// 账号登录
Widget _topWidget() {
return Container(
child: Stack(
children: <Widget>[
Positioned(
child: Container(
width: 130.0,
height: 8.0,
color: Color.fromRGBO(58, 184, 228, 1),
),
bottom: 5,
),
Text(
'账号登录',
style: TextStyle(
fontSize: 34,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
// textFiled
Widget _creatMyText(
{String placeholder,
bool obscureText = false,
TextEditingController controller,
keyboardType}) {
return TextField(
decoration: InputDecoration(
hintText: placeholder,
border: InputBorder.none,
),
obscureText: obscureText,
controller: controller,
keyboardType: keyboardType,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
margin: EdgeInsets.fromLTRB(30, 80, 30, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_backBtn(context),
SizedBox(
height: 50.0,
),
_topWidget(),
SizedBox(
height: 60.0,
),
_creatMyText(
placeholder: '请输入手机号码',
keyboardType: TextInputType.phone,
controller: mobileVC),
Divider(
color: Colors.black26,
),
SizedBox(
height: 20.0,
),
_creatMyText(
placeholder: '请输入密码',
keyboardType: TextInputType.phone,
obscureText: true,
controller: pwdVC),
Divider(
color: Colors.black26,
),
SizedBox(
height: 50,
),
iosTypeBtn(360, 60, '登录', _loginBtnClick,
textColor: Colors.white,
bgColor: Color.fromRGBO(58, 184, 228, 1),
fontSize: 16,
radius: 0),
],
),
),
);
}
}
3.界面写好后,就需要处理点击登录按钮后的网络请求了
TextEditingController mobileVC = TextEditingController();
TextEditingController pwdVC = TextEditingController();
String mobile = '';//用来存储页面textFiled的值
String pwd = '';//用来存储页面textFiled的值
@override
void initState() {
super.initState();
// 监听mobleTextField
mobileVC.addListener(() {
setState(() {
this.mobile = mobileVC.text;
});
});
// 监听pwdTextField
pwdVC.addListener(() {
setState(() {
this.pwd = pwdVC.text;
});
});
}
// 登录按钮点击事件
void _loginBtnClick() {
print('${this.mobile} - ${this.pwd}');
if (this.mobile.length != 11) {
Fluttertoast.showToast(msg: '手机号码格式错误', gravity: ToastGravity.CENTER);
return;
}
if (this.pwd.length == 0) {
Fluttertoast.showToast(msg: '密码不能为空', gravity: ToastGravity.CENTER);
return;
}
Map dataMap = {'mobile': this.mobile, 'password': generateMd5(this.pwd)};
cyw_getNetworkData('POST', servicePath['loginPath'], dataDic: dataMap)
.then((val) {
var result = json.decode(val.toString());
print(result);
if (result['returnCode'] == '0000') {
Fluttertoast.showToast(msg: '登录成功', gravity: ToastGravity.CENTER);
_saveUserInfo(result);
// push到homePage,并将前面路由清空
Navigator.pushNamedAndRemoveUntil(context, '/homePage', null);
} else {
Fluttertoast.showToast(msg: '登录失败', gravity: ToastGravity.CENTER);
}
});
}
// 存储用户信息
void _saveUserInfo(userInfo) async{
// SharedPreferences 类似iOS中NSUserDefaults
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('userId', userInfo['retnrnJson']['id']);
prefs.setBool('isLogin', true);
prefs.setString('userName', userInfo['retnrnJson']['userName']);
prefs.setString('password', userInfo['retnrnJson']['password']);
prefs.setString('mobile', userInfo['retnrnJson']['mobile']);
}
-
mobileVC.addListener
用来监听mobileTextField文本改变 -
Navigator.pushNamedAndRemoveUntil
push到下一个页面,并将前面路由清空。这样push导航栏上就不会有默认的返回按钮。
注册界面
效果图3注册界面跟登录界面很相似,复用的代码也很多,下面就直接粘代码了:
import 'package:flutter/material.dart';
import 'package:flutter_app05/config/service_methon.dart';
import 'dart:async';
import 'package:fluttertoast/fluttertoast.dart';
import '../tools/encrypt.dart';
import '../config/service_url.dart';
import 'dart:convert';
import '../tools/iosTypeButton.dart';
class RegistPage extends StatefulWidget {
RegistPage({Key key}) : super(key: key);
@override
_RegistPageState createState() => _RegistPageState();
}
class _RegistPageState extends State<RegistPage> {
TextEditingController _mobileVC = TextEditingController();
TextEditingController _codeVC = TextEditingController();
TextEditingController _pwdVC = TextEditingController();
String _mobile = '';
String _code = '';
String _pwd = '';
Timer _countdownTimer;
int _allTime = 59;//倒计时初始时间
String _getCodeString = '获取验证码';
bool _hasTime = true;//用来判断是否在倒计时中
@override
void initState() {
super.initState();
_mobileVC.addListener(() {
setState(() {
_mobile = _mobileVC.text;
});
});
_codeVC.addListener(() {
setState(() {
_code = _codeVC.text;
});
});
_pwdVC.addListener(() {
setState(() {
_pwd = _pwdVC.text;
});
});
}
@override
void dispose() {
// 页面销毁时销毁定时器
if (_countdownTimer != null) {
_countdownTimer = null;
_countdownTimer.cancel();
}
super.dispose();
}
// 返回按钮
Widget _backBtn(context) {
return Container(
height: 24.0,
width: 24.0,
child: InkWell(
child: Image.asset('images/back.png'),
onTap: () {
Navigator.of(context).pop();
},
),
);
}
// 账号登录
Widget _topWidget() {
return Container(
child: Stack(
children: <Widget>[
Positioned(
child: Container(
width: 80.0,
height: 8.0,
color: Color.fromRGBO(58, 184, 228, 1),
),
bottom: 5,
),
Text(
'注册',
style: TextStyle(
fontSize: 34,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
// textFiled
Widget _creatMyText(
{String placeholder,
bool obscureText = false,
TextEditingController controller,
keyboardType}) {
return TextField(
decoration: InputDecoration(
hintText: placeholder,
border: InputBorder.none,
),
obscureText: obscureText,
controller: controller,
keyboardType: keyboardType,
);
}
// 验证码
Widget _codeTextFiled() {
return Stack(
children: <Widget>[
_creatMyText(
placeholder: '请输入手机号码',
keyboardType: TextInputType.number,
controller: _mobileVC),
Positioned(
child: InkWell(
child: Text(
_getCodeString,
style: TextStyle(
color: Color.fromRGBO(58, 184, 228, 1), fontSize: 15),
),
onTap: _hasTime ? this._getCode : () {},
),
right: 20,
top: 10,
)
],
);
}
// 点击获取验证码
void _getCode() {
if (_mobile.length != 11) {
Fluttertoast.showToast(msg: '手机号格式错误');
return;
}
_reGetCountdown();
cyw_getNetworkData("POST", servicePath['sendCode'],
dataDic: {'mobile': this._mobile, 'type': '2'}).then((result) {
var data = json.decode(result.toString());
print(data);
if (data['returnCode'] == '0000') {
print('验证码已经发送');
this._reGetCountdown();
} else {
print('发送失败');
}
});
}
// 倒计时
void _reGetCountdown() {
setState(() {
if (_countdownTimer != null) {
return;
}
_hasTime = false;
// Timer的第一秒倒计时是有一点延迟的,为了立刻显示效果可以添加下一行。
_getCodeString = '${_allTime--}重新获取';
_countdownTimer = Timer.periodic(new Duration(seconds: 1), (timer) {
setState(() {
if (_allTime > 0) {
_getCodeString = '${_allTime--}S重新获取';
} else {
_getCodeString = '获取验证码';
_allTime = 59;
_countdownTimer.cancel();
_countdownTimer = null;
_hasTime = true;
}
});
});
});
}
// 点击注册按钮
_registBtnClick(){
if(_pwd.length == 0 || _code.length == 0){
Fluttertoast.showToast(msg: '填写信息不能为空');
return;
}
if (_mobile.length != 11) {
Fluttertoast.showToast(msg: '手机号格式错误');
return;
}
Map dataDic = {'mobile':_mobile,'userName':'','password':generateMd5(_pwd),'code':_code};
cyw_getNetworkData("POST", servicePath['signUpPath'],dataDic: dataDic).then((val){
var result = json.decode(val.toString());
print(result);
if (result['returnCode'] == '0000'){
Fluttertoast.showToast(msg: '注册成功');
Navigator.of(context).pop();
}else{
Fluttertoast.showToast(msg: result['returnMsg:']);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
margin: EdgeInsets.fromLTRB(30, 80, 30, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_backBtn(context),
SizedBox(
height: 50.0,
),
_topWidget(),
SizedBox(height: 60),
_codeTextFiled(),
Divider(
color: Colors.black26,
),
SizedBox(height: 20),
_creatMyText(
placeholder: '请输入验证码',
keyboardType: TextInputType.number,
controller: _codeVC),
Divider(
color: Colors.black26,
),
SizedBox(height: 20),
_creatMyText(
placeholder: '请输入密码',
keyboardType: TextInputType.number,
obscureText: true,
controller: _pwdVC),
Divider(
color: Colors.black26,
),
SizedBox(height: 50),
iosTypeBtn(360, 60, '注册', _registBtnClick,
textColor: Colors.white,
bgColor: Color.fromRGBO(58, 184, 228, 1),
fontSize: 16,
radius: 0),
],
)),
);
}
}
这里值得一提的是在Flutter定时器的使用,在periodic
方法中一定要记住再次调用setState
刷新页面UI。
_countdownTimer = Timer.periodic(new Duration(seconds: 1), (timer) {
setState(() {
if (_allTime > 0) {
_getCodeString = '${_allTime--}S重新获取';
} else {
_getCodeString = '获取验证码';
_allTime = 59;
_countdownTimer.cancel();
_countdownTimer = null;
_hasTime = true;
}
});
});
至此,从文件配置到登录注册界面已完成,如有造成了错误、误解的代码望大家谅解,最后希望能留下你宝贵的建议。
网友评论