本文主要針對(duì)有Dart和Flutter基礎(chǔ)的小伙伴,Dart和Flutter基礎(chǔ),廢話不多說,開擼。
文件配置
1.打開終端,cd到你想創(chuàng)建的根目錄,然后執(zhí)行flutter create xxxx(xxx為項(xiàng)目名),如顯示如下圖,即項(xiàng)目創(chuàng)建成功。

2.這里我使用的VSCode,用VSCode打開創(chuàng)建的項(xiàng)目,在lib目錄下創(chuàng)建config、pages、tools文件夾
- config 用來配置網(wǎng)絡(luò)請(qǐng)求以及路由文件
- pages 根據(jù)需求自己定義的頁(yè)面以及組件
- tools 存放自己封裝的工具類

路由配置
在config文件夾下創(chuàng)建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在這個(gè)Map對(duì)象中配置創(chuàng)建好的頁(yè)面 -
onGenerateRoute這個(gè)方法是谷歌官方為了配置路由為我們提供的,所以直接復(fù)制就可以了。有興趣的小伙伴可以研究下。
工具代碼封裝
Flutter為我們提供了很多優(yōu)秀的第三方庫(kù),這里我們使用的第三方網(wǎng)絡(luò)庫(kù)是Dio,當(dāng)然也有很多其他的庫(kù)如http,小伙伴可以https://pub.flutter-io.cn/查找
網(wǎng)絡(luò)工具
1.在pubspec.yaml的依賴中導(dǎo)入Dio

2.在config文件夾中創(chuàng)建
service_methon.dart,在service_methon.dart中對(duì)Dio按我們自己的需要做一點(diǎn)小處理
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網(wǎng)絡(luò)請(qǐng)求類型 -
url網(wǎng)絡(luò)請(qǐng)求地址 -
{Map dataDic}網(wǎng)絡(luò)請(qǐng)求參數(shù)(可選參數(shù)) -
Future是在未來某個(gè)時(shí)間獲得想要對(duì)象的一種手段。簡(jiǎn)單來說,就是我們能夠通過它在某個(gè)時(shí)間點(diǎn)獲得異步任務(wù)中返回的值。實(shí)際上,就是給 Future 設(shè)置回調(diào)函數(shù),當(dāng)異步任務(wù)執(zhí)行完成后,會(huì)調(diào)用回調(diào)函數(shù)。
cyw_getNetworkData此方法是一個(gè)異步的網(wǎng)絡(luò)請(qǐng)求方法,在Dart中方法名后要加上async關(guān)鍵字
3.config文件夾中創(chuàng)建service_url.dart,在service_url.dart配置URL

md5加密類
tools文件中創(chuàng)建encrypt.dart,Dart中直接為我們提供了MD5加密,導(dǎo)入相關(guān)庫(kù)就可以直接使用了
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);
// 這里其實(shí)就是 digest.toString()
return hex.encode(digest.bytes);
}
本地圖片配置
1.準(zhǔn)備本地圖片,在根目錄下創(chuàng)建images文件夾,并準(zhǔn)備2x,3x文件夾(高清圖準(zhǔn)備),在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
個(gè)人覺得Flutter本地圖片配置相對(duì)于iOS有點(diǎn)憨,不知道以后會(huì)不會(huì)有改進(jìn)。
以上基本準(zhǔn)備工作就完成了,接下來步入正題。
登錄注冊(cè)界面
登錄首頁(yè)界面

1.在tools文件夾下創(chuàng)建iosTypeButton.dart,并封裝此按鈕組件,個(gè)人喜歡iOS風(fēng)格,所以就創(chuàng)建了cupertino風(fēng)格的,代碼如下:
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.進(jìn)入main.dart,把Flutter自動(dòng)生成的代碼刪除,按照如下方式配置路由和界面
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固定的函數(shù)
);
}
}
3.創(chuàng)建登錄頁(yè)面的首頁(yè)界面,即loginMain頁(yè)面。在pages文件夾中創(chuàng)建loginMianPage.dart,并導(dǎo)入之前封裝好的按鈕組件,代碼如下:
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, '用戶注冊(cè)', () {
// Navigator.of(context).pushNamed('/registPage');
print('用戶注冊(cè)');
},
textColor: Color.fromRGBO(102, 102, 102, 1),
bgColor: Color.fromRGBO(243, 243, 243, 1),
fontSize: 16,
radius: 0),
],
),
),
);
}
}
4.點(diǎn)擊事件路由配置。在pages文件夾下面創(chuàng)建loginPage.dart和registPage.dart,然后在config-route.dart配置需要跳轉(zhuǎn)的路由:
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')打開就可以實(shí)現(xiàn)頁(yè)面的跳轉(zhuǎn)了。
至此,此界面功能基本完成。
登錄界面

1.此界面會(huì)用到2個(gè)第三庫(kù):
shared_preferences用來存儲(chǔ)用戶登錄信息,類似iOS中NSUserDefaults
fluttertoast 一個(gè)輕量化的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();
},
),
);
}
// 賬號(hào)登錄
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(
'賬號(hào)登錄',
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: '請(qǐng)輸入手機(jī)號(hào)碼',
keyboardType: TextInputType.phone,
controller: mobileVC),
Divider(
color: Colors.black26,
),
SizedBox(
height: 20.0,
),
_creatMyText(
placeholder: '請(qǐng)輸入密碼',
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.界面寫好后,就需要處理點(diǎn)擊登錄按鈕后的網(wǎng)絡(luò)請(qǐng)求了
TextEditingController mobileVC = TextEditingController();
TextEditingController pwdVC = TextEditingController();
String mobile = '';//用來存儲(chǔ)頁(yè)面textFiled的值
String pwd = '';//用來存儲(chǔ)頁(yè)面textFiled的值
@override
void initState() {
super.initState();
// 監(jiān)聽mobleTextField
mobileVC.addListener(() {
setState(() {
this.mobile = mobileVC.text;
});
});
// 監(jiān)聽pwdTextField
pwdVC.addListener(() {
setState(() {
this.pwd = pwdVC.text;
});
});
}
// 登錄按鈕點(diǎn)擊事件
void _loginBtnClick() {
print('${this.mobile} - ${this.pwd}');
if (this.mobile.length != 11) {
Fluttertoast.showToast(msg: '手機(jī)號(hào)碼格式錯(cuò)誤', 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);
}
});
}
// 存儲(chǔ)用戶信息
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用來監(jiān)聽mobileTextField文本改變 -
Navigator.pushNamedAndRemoveUntilpush到下一個(gè)頁(yè)面,并將前面路由清空。這樣push導(dǎo)航欄上就不會(huì)有默認(rèn)的返回按鈕。
注冊(cè)界面

注冊(cè)界面跟登錄界面很相似,復(fù)用的代碼也很多,下面就直接粘代碼了:
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;//倒計(jì)時(shí)初始時(shí)間
String _getCodeString = '獲取驗(yàn)證碼';
bool _hasTime = true;//用來判斷是否在倒計(jì)時(shí)中
@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() {
// 頁(yè)面銷毀時(shí)銷毀定時(shí)器
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();
},
),
);
}
// 賬號(hào)登錄
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(
'注冊(cè)',
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,
);
}
// 驗(yàn)證碼
Widget _codeTextFiled() {
return Stack(
children: <Widget>[
_creatMyText(
placeholder: '請(qǐng)輸入手機(jī)號(hào)碼',
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,
)
],
);
}
// 點(diǎn)擊獲取驗(yàn)證碼
void _getCode() {
if (_mobile.length != 11) {
Fluttertoast.showToast(msg: '手機(jī)號(hào)格式錯(cuò)誤');
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('驗(yàn)證碼已經(jīng)發(fā)送');
this._reGetCountdown();
} else {
print('發(fā)送失敗');
}
});
}
// 倒計(jì)時(shí)
void _reGetCountdown() {
setState(() {
if (_countdownTimer != null) {
return;
}
_hasTime = false;
// Timer的第一秒倒計(jì)時(shí)是有一點(diǎn)延遲的,為了立刻顯示效果可以添加下一行。
_getCodeString = '${_allTime--}重新獲取';
_countdownTimer = Timer.periodic(new Duration(seconds: 1), (timer) {
setState(() {
if (_allTime > 0) {
_getCodeString = '${_allTime--}S重新獲取';
} else {
_getCodeString = '獲取驗(yàn)證碼';
_allTime = 59;
_countdownTimer.cancel();
_countdownTimer = null;
_hasTime = true;
}
});
});
});
}
// 點(diǎn)擊注冊(cè)按鈕
_registBtnClick(){
if(_pwd.length == 0 || _code.length == 0){
Fluttertoast.showToast(msg: '填寫信息不能為空');
return;
}
if (_mobile.length != 11) {
Fluttertoast.showToast(msg: '手機(jī)號(hào)格式錯(cuò)誤');
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: '注冊(cè)成功');
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: '請(qǐng)輸入驗(yàn)證碼',
keyboardType: TextInputType.number,
controller: _codeVC),
Divider(
color: Colors.black26,
),
SizedBox(height: 20),
_creatMyText(
placeholder: '請(qǐng)輸入密碼',
keyboardType: TextInputType.number,
obscureText: true,
controller: _pwdVC),
Divider(
color: Colors.black26,
),
SizedBox(height: 50),
iosTypeBtn(360, 60, '注冊(cè)', _registBtnClick,
textColor: Colors.white,
bgColor: Color.fromRGBO(58, 184, 228, 1),
fontSize: 16,
radius: 0),
],
)),
);
}
}
這里值得一提的是在Flutter定時(shí)器的使用,在periodic方法中一定要記住再次調(diào)用setState刷新頁(yè)面UI。
_countdownTimer = Timer.periodic(new Duration(seconds: 1), (timer) {
setState(() {
if (_allTime > 0) {
_getCodeString = '${_allTime--}S重新獲取';
} else {
_getCodeString = '獲取驗(yàn)證碼';
_allTime = 59;
_countdownTimer.cancel();
_countdownTimer = null;
_hasTime = true;
}
});
});
至此,從文件配置到登錄注冊(cè)界面已完成,如有造成了錯(cuò)誤、誤解的代碼望大家諒解,最后希望能留下你寶貴的建議。