优化输入体验
在进行下一步之前,我们先优化一下注册的体验:
- 正在输入注册信息时,点击屏幕空白部分,清除当前文本输入框的焦点,同时收起键盘。
- 正在输入注册信息时,直接收起键盘,再点击空白部分,清除当前文本输入框的焦点。
- 不在用户输入时直接判断并显示错误提示信息,而是在用户输入完成以及点击加入按钮时判断并显示错误提示信息。
- 在每一个输入框下方都显示帮助信息,提示用户输入什么内容。
首先我们把SignUpState
类的_correctUsername
和_correctUsername
变量改为bool
类型。并把用户名和密码的判断逻辑合并到_checkInput
方法中。
class SignUpState extends State<SignUp> {
bool _correctPhone = true;
bool _correctUsername = true;
bool _correctPassword = true;
//...
void _checkInput() {
if (_phoneController.text.isNotEmpty &&
(_phoneController.text.trim().length < 7 ||
_phoneController.text.trim().length > 12)) {
_correctPhone = false;
} else {
_correctPhone = true;
}
if (_usernameController.text.isNotEmpty &&
_usernameController.text.trim().length < 2) {
_correctUsername = false;
} else {
_correctUsername = true;
}
if (_passwordController.text.isNotEmpty &&
_passwordController.text.trim().length < 6) {
_correctPassword = false;
} else {
_correctPassword = true;
}
setState(() {});
}
//...
}
因为我们将使用手机号作为用户的唯一ID存储在数据库中,所以在上面的代码中,我们增加了一个_correctPhone变量与判断手机号长度的逻辑。
在SignUpState
类的build
里添加helperText
属性,它能在文本框下方显示输入帮助信息。使用onSubmitted
代替onChanged
属性,它会在用户点击键盘上的完成按钮时触发事件,也就是上面合并修改的_checkInput
方法。
class SignUpState extends State<SignUp> {
//...
new TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: new InputDecoration(
helperText: 'Your unique ID.',
hintText: 'Phone',
errorText: _correctPhone
? null
: 'phone length is 7 to 12 bits.',
icon: new Icon(
Icons.phone,
),
),
onSubmitted: (value) {
_checkInput();
},
),
new TextField(
controller: _usernameController,
decoration: new InputDecoration(
helperText: "What's your name?",
hintText: 'Username',
errorText: _correctUsername
? null
: 'Username length is less than 2 bits.',
icon: new Icon(
Icons.account_circle,
),
),
onSubmitted: (value) {
_checkInput();
},
),
new TextField(
controller: _passwordController,
obscureText: true,
keyboardType: TextInputType.number,
decoration: new InputDecoration(
helperText: 'Your landing password.',
hintText: 'Password',
errorText: _correctPassword
? null
: 'Password length is less than 6 bits.',
icon: new Icon(
Icons.lock_outline,
),
),
onSubmitted: (value) {
_checkInput();
},
),
//...
}
然后我们需要在把有背景图片的Container
控件包装在GestureDetector
控件中,再添加点击事件,清除文本输入框的焦点,然后判断输入内容。
class SignUpState extends State<SignUp> {
//...
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Stack(children: <Widget>[
new Opacity(
opacity: 0.3,
child: new GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(new FocusNode());
_checkInput();
},
child: new Container(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new ExactAssetImage('images/sign_up_background.jpg'),
fit: BoxFit.cover,
),
),
))),
//...
}
//...
}
最后我们在用户点击加入按钮提交注册信息之前,先清除文本输入框的焦点并判断输入内容。
class SignUpState extends State<SignUp> {
//...
Future _handleSubmitted() async {
FocusScope.of(context).requestFocus(new FocusNode());
_checkInput();
//...
}
//...
}
添加等待动画
用户在提交注册请求后,需要等待服务端返回请求结果,这一等待时间会根据当前网络的因素或长或短。所以我们现在要增加一个等待动画,使用CircularProgressIndicator
控件实现,其效果是一个不断转动的圆形。在等待期间我们不需要用户有其他操作,因此需要一个新屏幕。
class ShowAwait extends StatefulWidget {
ShowAwait(this.requestCallback);
final Future<int> requestCallback;
@override
_ShowAwaitState createState() => new _ShowAwaitState();
}
class _ShowAwaitState extends State<ShowAwait> {
@override
initState() {
super.initState();
new Timer(new Duration(seconds: 2), () {
widget.requestCallback.then((int onValue) {
Navigator.of(context).pop(onValue);
});
});
}
@override
Widget build(BuildContext context) {
return new Center(
child: new CircularProgressIndicator(),
);
}
}
上面代码中,在initState
方法中有一个Timer
类,它能设置定时任务,定时执行一次或者每隔一段时间执行一次,我们在这里设置的是定时2秒执行一次传入requestCallbac
方法。同时requestCallbac
方法需要返回一个int
值,该值用于返回服务端的处理结果。执行完毕后使用Navigator.of(context).pop
返回结果并关闭等待屏幕。
提交注册信息
然后回到sign_up.dart
文件,我们要开始写服务端的代码,关于如何使用Firebase数据库,可以查看《Flutter进阶—Firebase数据库实例》,我们保存用户信息的数据名称是users
,因此需要先连接上Firebase数据库。
class SignUpState extends State<SignUp> {
//...
final reference = FirebaseDatabase.instance.reference().child('users');
//...
}
现在我们修改一下_userLogUp
方法,在注册之前要先验证用户注册的手机号码是否已经存在,如果已经被注册,则返回0
。确定当前手机号码未被注册之后,才提交用户注册信息到数据库,并返回1
。
class SignUpState extends State<SignUp> {
//...
Future<int> _userLogUp(String username, String password,
{String email, String phone}) async {
return await reference
.child(_phoneController.text)
.once()
.then((DataSnapshot onValue) {
if (onValue.value == null) {
reference.child(phone).set({
'name': username,
'password': password,
'email': email,
'phone': phone,
});
return 1;
} else {
return 0;
}
});
}
//...
}
再修改一下_handleSubmitted
方法,在用户点击注册按钮时弹出ShowAwait
创建的等待屏幕,并把上面的_userLogUp
方法传给ShowAwait
实例。如果返回0
,则提示用户,如果返回1
,则返回并传递用户的手机号、密码到登陆屏幕。
class SignUpState extends State<SignUp> {
//...
void _handleSubmitted() {
FocusScope.of(context).requestFocus(new FocusNode());
_checkInput();
if (_usernameController.text == '' || _passwordController.text == '') {
showMessage(context, "The registration information is incomplete!");
return;
} else if (!_correctUsername || !_correctPassword || !_correctPhone) {
showMessage(context, "The registration information is incomplete!");
return;
}
showDialog<int>(
context: context,
barrierDismissible: false,
child: new ShowAwait(_userLogUp(
_usernameController.text, _passwordController.text,
email: _emailController.text,
phone: _phoneController.text))).then((int onValue) {
if (onValue == 0) {
showMessage(context, "This number has been registered!");
} else if (onValue == 1) {
Navigator
.of(context)
.pop([_phoneController.text, _passwordController.text]);
}
});
}
//...
}