Flutter實戰一Flutter聊天應用(十四)
優化輸入體驗
在進行下一步之前,我們先優化一下注冊的體驗:
- 正在輸入註冊資訊時,點選螢幕空白部分,清除當前文字輸入框的焦點,同時收起鍵盤。
- 正在輸入註冊資訊時,直接收起鍵盤,再點選空白部分,清除當前文字輸入框的焦點。
- 不在使用者輸入時直接判斷並顯示錯誤提示資訊,而是在使用者輸入完成以及點選加入按鈕時判斷並顯示錯誤提示資訊。
- 在每一個輸入框下方都顯示幫助資訊,提示使用者輸入什麼內容。
首先我們把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]);
}
});
}
//...
}