在开发Flutter倒计时,setState刷新会造成页面的闪烁,如图

一、setState全局刷新
1、setState页面问题分析
在flutter中常用的刷新方法有setState,然后这个会造成整个页面刷新,特别是绘制需要时间的组件会闪烁.
2、setState页面源码
login_demo_page.dart
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
class LoginDemoPage extends StatefulWidget {
LoginDemoPage({Key key});
State<StatefulWidget> createState() {
return _LoginPageState();
}
}
class _LoginPageState extends State<LoginDemoPage> {
int _seconds = 0;
bool _loadingImageUrl = true;
String _imageUrl =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAAaCAIAAABw/jbvAAABjElEQVR42s2Y7Q6DIAxF2dxm9v4P7JaYYNeP2wJFZ/ihFS2HlrZQtoGrlDL7W7VbFcq3VLLfl23sGoR0P7c6xCGvJIxwuq8sIx9TUKXv17029Y9Wh3FIwBmBx3buJHw8V9lUfUt57Q0M5basoLkehGdBJ5SQLp4FaRHS+27CiAf1E9K3AJISAkgmiRO6kArh9wMGWR8rofwRI6yKmwirsOJFFnkzIUVSCdWFKgl33YDQGhw1YHfaRLE0eM9+pxJKG7IYiAlTMplOSB2VIlmEch0GCdXpP5WQIamEFE+aRRIyNta/1T/9NGsRqjxMoqYK5n4uoZU50moJa/SuxMJjnE2E0j8TqkI1W1hhykr6QIFKKMMMSxKJVSHyUosQ1DQg0sjGtIAsP7SD6ahrZxDiGHM2YUoAwHuCRKWlo3LPIuze2qcR5mrqhnRL2WQvdXfW6T6cFkubIC804ywv5cc7/034M9rgaU9k1zPjOMs9p3APfkpcx+CKDx4r5TpqdNDHfHhbu+DUgmvQjLLbBy9uOkzsE/1jAAAAAElFTkSuQmCC';
final phoneFormKey = GlobalKey<FormState>();
final imageFormKey = GlobalKey<FormState>();
final codeFormKey = GlobalKey<FormState>();
final Map<String, String> formValue = new HashMap();
Timer _timer;
Color LabelBlackColor = Color(0xFF1A1A1A);
void dispose() {
super.dispose();
if (_timer != null) {
_timer.cancel();
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('登录示例demo'),
),
body: buildBody(),
);
}
sendCode() async {
_seconds = 60;
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_seconds == 0) {
timer.cancel(); // 取消重复计时
return;
}
_seconds--;
if (mounted) {
setState(() {});
}
});
}
Widget buildBody() {
return Container(
padding: EdgeInsets.only(right: 20, top: 20),
child: ListView(
children: [
Padding(
padding: EdgeInsets.only(left: 20),
child: Text(
'快捷登录注册',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 26),
),
),
SizedBox(height: 30),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: phoneFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'+86',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
hintText: '请输入手机账号',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
),
onSaved: (mobile) {
if (mobile == null || mobile.isEmpty == true) {
return;
}
formValue['mobile'] = mobile;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: codeFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入图形验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_loadingImageUrl) {
return;
} else {
_loadingImageUrl = true;
if (mounted) {
setState(() {});
}
}
},
child: Container(
width: 100,
height: 30,
child: Image.memory(
base64Decode(_imageUrl
.split(',')[1]
.replaceAll('\r', '')
.replaceAll('\n', '')),
width: 100,
),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['imageCode'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: imageFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入短信验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_seconds == 0) {
phoneFormKey.currentState.save();
codeFormKey.currentState.save();
sendCode();
}
},
child: Center(
widthFactor: 1,
child: Text(
_seconds == 0 ? '获取验证码' : '$_seconds秒',
style: TextStyle(fontSize: 16),
),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['code'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: GestureDetector(
onTap: () {
phoneFormKey.currentState.save();
imageFormKey.currentState.save();
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 48,
color: LabelBlackColor,
alignment: Alignment.center,
child: Text(
'登录/注册',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
),
],
),
);
}
}
二、局部刷新
此时我们需要仅仅刷新倒计时组件即可,也就是使用局部刷新来解决问题
1、抽离成组件state(刷新部分抽离)
与React类似,咱们把想刷新的部分抽离成组件,因为React、Flutter的state都是针对整个class的。
1.1、页面调用子组件方法
这里需要用到父组件调用子组件的发送验证码方法,
首先在子组件中定义key
GlobalKey<_CutdownTimeWidgetState> childKey = GlobalKey();
同时子组件的key要通过super传递给父类,这个不能漏
CutdownTimeWidget({
Key key,
}) : super(key: key);
然后父组件把定义的key导包进来作为入参即可
CutdownTimeWidget(key: childKey)
最后可以方便的调用子组件方法
if (childKey.currentState?.seconds == 0) {
childKey.currentState?.sendCode();
}
1.2、组件源码
把刷新相关的功能集成到子组件cutdown_time_widge.dart
import 'dart:async';
import 'package:flutter/material.dart';
GlobalKey<_CutdownTimeWidgetState> childKey = GlobalKey();
class CutdownTimeWidget extends StatefulWidget {
CutdownTimeWidget({
Key key,
}) : super(key: key);
State<StatefulWidget> createState() {
return _CutdownTimeWidgetState();
}
}
class _CutdownTimeWidgetState extends State<CutdownTimeWidget> {
int seconds = 0;
Timer _timer;
void dispose() {
super.dispose();
if (_timer != null) {
_timer.cancel();
}
}
sendCode() async {
seconds = 60;
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (seconds == 0) {
timer.cancel(); // 取消重复计时
return;
}
seconds--;
if (mounted) {
setState(() {});
}
});
}
Widget build(BuildContext context) {
return Text(
seconds == 0 ? '获取验证码' : '$seconds秒',
style: TextStyle(fontSize: 16),
);
}
}
1.3、页面源码
页面login_demo_page.dart
直接调用组件的方法,即实现了局部刷新解决闪烁问题。
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:scroll_tabbar_sample/widgets/cutdown_time_widge.dart';
class LoginDemoPage extends StatefulWidget {
LoginDemoPage({Key key});
State<StatefulWidget> createState() {
return _LoginPageState();
}
}
class _LoginPageState extends State<LoginDemoPage> {
bool _loadingImageUrl = true;
String _imageUrl =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAAaCAIAAABw/jbvAAABjElEQVR42s2Y7Q6DIAxF2dxm9v4P7JaYYNeP2wJFZ/ihFS2HlrZQtoGrlDL7W7VbFcq3VLLfl23sGoR0P7c6xCGvJIxwuq8sIx9TUKXv17029Y9Wh3FIwBmBx3buJHw8V9lUfUt57Q0M5basoLkehGdBJ5SQLp4FaRHS+27CiAf1E9K3AJISAkgmiRO6kArh9wMGWR8rofwRI6yKmwirsOJFFnkzIUVSCdWFKgl33YDQGhw1YHfaRLE0eM9+pxJKG7IYiAlTMplOSB2VIlmEch0GCdXpP5WQIamEFE+aRRIyNta/1T/9NGsRqjxMoqYK5n4uoZU50moJa/SuxMJjnE2E0j8TqkI1W1hhykr6QIFKKMMMSxKJVSHyUosQ1DQg0sjGtIAsP7SD6ahrZxDiGHM2YUoAwHuCRKWlo3LPIuze2qcR5mrqhnRL2WQvdXfW6T6cFkubIC804ywv5cc7/034M9rgaU9k1zPjOMs9p3APfkpcx+CKDx4r5TpqdNDHfHhbu+DUgmvQjLLbBy9uOkzsE/1jAAAAAElFTkSuQmCC';
final phoneFormKey = GlobalKey<FormState>();
final imageFormKey = GlobalKey<FormState>();
final codeFormKey = GlobalKey<FormState>();
final Map<String, String> formValue = new HashMap();
Color LabelBlackColor = Color(0xFF1A1A1A);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('登录示例demo'),
),
body: buildBody(),
);
}
Widget buildBody() {
return Container(
padding: EdgeInsets.only(right: 20, top: 20),
child: ListView(
children: [
Padding(
padding: EdgeInsets.only(left: 20),
child: Text(
'快捷登录注册',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 26),
),
),
SizedBox(height: 30),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: phoneFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'+86',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
hintText: '请输入手机账号',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
),
onSaved: (mobile) {
if (mobile == null || mobile.isEmpty == true) {
return;
}
formValue['mobile'] = mobile;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: codeFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入图形验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_loadingImageUrl) {
return;
} else {
_loadingImageUrl = true;
if (mounted) {
setState(() {});
}
}
},
child: Container(
width: 100,
height: 30,
child: Image.memory(
base64Decode(_imageUrl
.split(',')[1]
.replaceAll('\r', '')
.replaceAll('\n', '')),
width: 100,
),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['imageCode'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: imageFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入短信验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (childKey.currentState?.seconds == 0) {
phoneFormKey.currentState.save();
codeFormKey.currentState.save();
childKey.currentState?.sendCode();
}
},
child: Center(
widthFactor: 1,
child: CutdownTimeWidget(key: childKey),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['code'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: GestureDetector(
onTap: () {
phoneFormKey.currentState.save();
imageFormKey.currentState.save();
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 48,
color: LabelBlackColor,
alignment: Alignment.center,
child: Text(
'登录/注册',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
),
],
),
);
}
}
2、使用ValueNotifier(通知方式)
通过抽离组件确实能实现局部刷新,但是书写臃肿。
ValueNotifier的方式为通知的方式
2.1、ValueNotifier使用
首先使用ValueNotifier对象替换之前定义的int _seconds = 0;
ValueNotifier secondNotifier = ValueNotifier<int>(0);
对于需要操作_seconds变量的改为操作secondNotifier.value
secondNotifier.value = 60;
最后对于绑定视图的组件使用ValueListenableBuilder包裹起来即可
ValueListenableBuilder(
valueListenable: secondNotifier,
builder:
(BuildContext context, value, Widget child) {
return Text(
value == 0 ? '获取验证码' : '$value秒',
style: TextStyle(fontSize: 16),
);
},
)
2.2、页面源码
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
class LoginDemoPage extends StatefulWidget {
LoginDemoPage({Key key});
State<StatefulWidget> createState() {
return _LoginPageState();
}
}
class _LoginPageState extends State<LoginDemoPage> {
ValueNotifier secondNotifier = ValueNotifier<int>(0);
bool _loadingImageUrl = true;
String _imageUrl =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAAaCAIAAABw/jbvAAABjElEQVR42s2Y7Q6DIAxF2dxm9v4P7JaYYNeP2wJFZ/ihFS2HlrZQtoGrlDL7W7VbFcq3VLLfl23sGoR0P7c6xCGvJIxwuq8sIx9TUKXv17029Y9Wh3FIwBmBx3buJHw8V9lUfUt57Q0M5basoLkehGdBJ5SQLp4FaRHS+27CiAf1E9K3AJISAkgmiRO6kArh9wMGWR8rofwRI6yKmwirsOJFFnkzIUVSCdWFKgl33YDQGhw1YHfaRLE0eM9+pxJKG7IYiAlTMplOSB2VIlmEch0GCdXpP5WQIamEFE+aRRIyNta/1T/9NGsRqjxMoqYK5n4uoZU50moJa/SuxMJjnE2E0j8TqkI1W1hhykr6QIFKKMMMSxKJVSHyUosQ1DQg0sjGtIAsP7SD6ahrZxDiGHM2YUoAwHuCRKWlo3LPIuze2qcR5mrqhnRL2WQvdXfW6T6cFkubIC804ywv5cc7/034M9rgaU9k1zPjOMs9p3APfkpcx+CKDx4r5TpqdNDHfHhbu+DUgmvQjLLbBy9uOkzsE/1jAAAAAElFTkSuQmCC';
final phoneFormKey = GlobalKey<FormState>();
final imageFormKey = GlobalKey<FormState>();
final codeFormKey = GlobalKey<FormState>();
final Map<String, String> formValue = new HashMap();
Timer _timer;
Color LabelBlackColor = Color(0xFF1A1A1A);
void dispose() {
super.dispose();
if (_timer != null) {
_timer.cancel();
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('登录示例demo'),
),
body: buildBody(),
);
}
sendCode() async {
secondNotifier.value = 60;
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (secondNotifier.value == 0) {
timer.cancel(); // 取消重复计时
return;
}
secondNotifier.value--;
});
}
Widget buildBody() {
return Container(
padding: EdgeInsets.only(right: 20, top: 20),
child: ListView(
children: [
Padding(
padding: EdgeInsets.only(left: 20),
child: Text(
'快捷登录注册',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 26),
),
),
SizedBox(height: 30),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: phoneFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'+86',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
hintText: '请输入手机账号',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
),
onSaved: (mobile) {
if (mobile == null || mobile.isEmpty == true) {
return;
}
formValue['mobile'] = mobile;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: codeFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入图形验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_loadingImageUrl) {
return;
} else {
_loadingImageUrl = true;
}
},
child: Container(
width: 100,
height: 30,
child: Image.memory(
base64Decode(_imageUrl
.split(',')[1]
.replaceAll('\r', '')
.replaceAll('\n', '')),
width: 100,
),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['imageCode'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: imageFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入短信验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (secondNotifier.value == 0) {
phoneFormKey.currentState.save();
codeFormKey.currentState.save();
sendCode();
}
},
child: Center(
widthFactor: 1,
child: ValueListenableBuilder(
valueListenable: secondNotifier,
builder:
(BuildContext context, value, Widget child) {
return Text(
value == 0 ? '获取验证码' : '$value秒',
style: TextStyle(fontSize: 16),
);
},
),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['code'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: GestureDetector(
onTap: () {
phoneFormKey.currentState.save();
imageFormKey.currentState.save();
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 48,
color: LabelBlackColor,
alignment: Alignment.center,
child: Text(
'登录/注册',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
),
],
),
);
}
}
3、使用Stream的(通知方式)
3.1、StreamController使用
声明StreamController对象
StreamController<String> secondStreamController= StreamController<String>();
对于需要操作_seconds变量不变,操作完成之后需要刷新视图的用secondStreamController.add即可,注意接受参数需要用toString()字符串化。
secondStreamController.add(_seconds.toString());
最后对于绑定视图的组件使用StreamBuilder包裹起来即可
StreamBuilder(
stream: secondStreamController.stream,
initialData: _seconds,
builder: (context, AsyncSnapshot snapshot) {
return Text(
snapshot.data == 0 ? '获取验证码' : '${snapshot.data}秒',
style: TextStyle(fontSize: 16),
);
},
)
3.2、页面源码
login_demo_page.dart
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
class LoginDemoPage extends StatefulWidget {
LoginDemoPage({Key key});
State<StatefulWidget> createState() {
return _LoginPageState();
}
}
class _LoginPageState extends State<LoginDemoPage> {
int _seconds = 0;
StreamController<String> secondStreamController= StreamController<String>();
bool _loadingImageUrl = true;
String _imageUrl =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAAaCAIAAABw/jbvAAABjElEQVR42s2Y7Q6DIAxF2dxm9v4P7JaYYNeP2wJFZ/ihFS2HlrZQtoGrlDL7W7VbFcq3VLLfl23sGoR0P7c6xCGvJIxwuq8sIx9TUKXv17029Y9Wh3FIwBmBx3buJHw8V9lUfUt57Q0M5basoLkehGdBJ5SQLp4FaRHS+27CiAf1E9K3AJISAkgmiRO6kArh9wMGWR8rofwRI6yKmwirsOJFFnkzIUVSCdWFKgl33YDQGhw1YHfaRLE0eM9+pxJKG7IYiAlTMplOSB2VIlmEch0GCdXpP5WQIamEFE+aRRIyNta/1T/9NGsRqjxMoqYK5n4uoZU50moJa/SuxMJjnE2E0j8TqkI1W1hhykr6QIFKKMMMSxKJVSHyUosQ1DQg0sjGtIAsP7SD6ahrZxDiGHM2YUoAwHuCRKWlo3LPIuze2qcR5mrqhnRL2WQvdXfW6T6cFkubIC804ywv5cc7/034M9rgaU9k1zPjOMs9p3APfkpcx+CKDx4r5TpqdNDHfHhbu+DUgmvQjLLbBy9uOkzsE/1jAAAAAElFTkSuQmCC';
final phoneFormKey = GlobalKey<FormState>();
final imageFormKey = GlobalKey<FormState>();
final codeFormKey = GlobalKey<FormState>();
final Map<String, String> formValue = new HashMap();
Timer _timer;
Color LabelBlackColor = Color(0xFF1A1A1A);
void dispose() {
super.dispose();
if (_timer != null) {
_timer.cancel();
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('登录示例demo'),
),
body: buildBody(),
);
}
sendCode() async {
_seconds = 60;
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_seconds == 0) {
timer.cancel(); // 取消重复计时
return;
}
_seconds--;
secondStreamController.add(_seconds.toString());
});
}
Widget buildBody() {
return Container(
padding: EdgeInsets.only(right: 20, top: 20),
child: ListView(
children: [
Padding(
padding: EdgeInsets.only(left: 20),
child: Text(
'快捷登录注册',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 26),
),
),
SizedBox(height: 30),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: phoneFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'+86',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
hintText: '请输入手机账号',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
),
onSaved: (mobile) {
if (mobile == null || mobile.isEmpty == true) {
return;
}
formValue['mobile'] = mobile;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: codeFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入图形验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_loadingImageUrl) {
return;
} else {
_loadingImageUrl = true;
}
},
child: Container(
width: 100,
height: 30,
child: Image.memory(
base64Decode(_imageUrl
.split(',')[1]
.replaceAll('\r', '')
.replaceAll('\n', '')),
width: 100,
),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['imageCode'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: imageFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入短信验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_seconds == 0) {
phoneFormKey.currentState.save();
codeFormKey.currentState.save();
sendCode();
}
},
child: Center(
widthFactor: 1,
child: StreamBuilder(
stream: secondStreamController.stream,
initialData: _seconds,
builder: (context, AsyncSnapshot snapshot) {
return Text(
snapshot.data == 0 ? '获取验证码' : '${snapshot.data}秒',
style: TextStyle(fontSize: 16),
);
},
),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['code'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: GestureDetector(
onTap: () {
phoneFormKey.currentState.save();
imageFormKey.currentState.save();
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 48,
color: LabelBlackColor,
alignment: Alignment.center,
child: Text(
'登录/注册',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
),
],
),
);
}
}
4、使用InHeritedWidget(数据共享方式)
InheritedWidget 组件是功能型组件,提供了沿树向下,共享数据的功能,即子组件可以获取父组件(InheritedWidget 的子类)的数据
InHeritedWidget的使用固定,可用于共享state
4.1、InHeritedWidget使用
首先我们需要创建我们的共享数据类TimerInheritedWidget类,写法相对固定,这里seconds即是我们的变量
timer_inherited_widget.dart
class TimerInheritedWidget extends InheritedWidget {
final int seconds;
TimerInheritedWidget(this.seconds, Widget child) : super(child: child);
static TimerInheritedWidget of(BuildContext context, {bool rebuild = true}) {
if (rebuild) {
final TimerInheritedWidget widget=context.dependOnInheritedWidgetOfExactType<TimerInheritedWidget>();
return widget;
}
return context.findAncestorWidgetOfExactType<TimerInheritedWidget>();
}
bool updateShouldNotify(TimerInheritedWidget old) {
return seconds != old.seconds;
}
}
然后使用在页面中使用我们的组件,通过builder即可实现局部刷新
TimerInheritedWidget(
_seconds,
Builder(
builder: (BuildContext innerContext) {
return Container(
child:Text(
_seconds == 0 ? '获取验证码' : '$_seconds秒',
style: TextStyle(fontSize: 16),
)
);
},
)
)
如果要获取共享state中的值(注意:这里是上下文必须是build的上下文,即innerContext)
TimerInheritedWidget.of(innerContext).seconds
4.2、页面源码
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:scroll_tabbar_sample/widgets/timer_inherited_widget.dart';
class LoginDemoPage extends StatefulWidget {
LoginDemoPage({Key key});
State<StatefulWidget> createState() {
return _LoginPageState();
}
}
class _LoginPageState extends State<LoginDemoPage> {
int _seconds = 0;
StreamController<String> secondStreamController = StreamController<String>();
bool _loadingImageUrl = true;
String _imageUrl =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAAaCAIAAABw/jbvAAABjElEQVR42s2Y7Q6DIAxF2dxm9v4P7JaYYNeP2wJFZ/ihFS2HlrZQtoGrlDL7W7VbFcq3VLLfl23sGoR0P7c6xCGvJIxwuq8sIx9TUKXv17029Y9Wh3FIwBmBx3buJHw8V9lUfUt57Q0M5basoLkehGdBJ5SQLp4FaRHS+27CiAf1E9K3AJISAkgmiRO6kArh9wMGWR8rofwRI6yKmwirsOJFFnkzIUVSCdWFKgl33YDQGhw1YHfaRLE0eM9+pxJKG7IYiAlTMplOSB2VIlmEch0GCdXpP5WQIamEFE+aRRIyNta/1T/9NGsRqjxMoqYK5n4uoZU50moJa/SuxMJjnE2E0j8TqkI1W1hhykr6QIFKKMMMSxKJVSHyUosQ1DQg0sjGtIAsP7SD6ahrZxDiGHM2YUoAwHuCRKWlo3LPIuze2qcR5mrqhnRL2WQvdXfW6T6cFkubIC804ywv5cc7/034M9rgaU9k1zPjOMs9p3APfkpcx+CKDx4r5TpqdNDHfHhbu+DUgmvQjLLbBy9uOkzsE/1jAAAAAElFTkSuQmCC';
final phoneFormKey = GlobalKey<FormState>();
final imageFormKey = GlobalKey<FormState>();
final codeFormKey = GlobalKey<FormState>();
final Map<String, String> formValue = new HashMap();
Timer _timer;
Color LabelBlackColor = Color(0xFF1A1A1A);
void dispose() {
super.dispose();
if (_timer != null) {
_timer.cancel();
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('登录示例demo'),
),
body: buildBody(),
);
}
sendCode() async {
setState(() {
_seconds = 60;
});
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_seconds == 0) {
timer.cancel(); // 取消重复计时
return;
}
setState(() {
_seconds--;
});
secondStreamController.add(_seconds.toString());
});
}
Widget buildBody() {
return TimerInheritedWidget(
_seconds,
Builder(
builder: (BuildContext innerContext) {
return Container(
padding: EdgeInsets.only(right: 20, top: 20),
child: ListView(
children: [
Padding(
padding: EdgeInsets.only(left: 20),
child: Text(
'快捷登录注册',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 26),
),
),
SizedBox(height: 30),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: phoneFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'+86',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
hintText: '请输入手机账号',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
),
onSaved: (mobile) {
if (mobile == null || mobile.isEmpty == true) {
return;
}
formValue['mobile'] = mobile;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: codeFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入图形验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_loadingImageUrl) {
return;
} else {
_loadingImageUrl = true;
}
},
child: Container(
width: 100,
height: 30,
child: Image.memory(
base64Decode(_imageUrl
.split(',')[1]
.replaceAll('\r', '')
.replaceAll('\n', '')),
width: 100,
),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['imageCode'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: imageFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入短信验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_seconds == 0) {
phoneFormKey.currentState.save();
codeFormKey.currentState.save();
sendCode();
}
},
child: Center(
widthFactor: 1,
child: Text(
_seconds == 0 ? '获取验证码' : '$_seconds秒',
style: TextStyle(fontSize: 16),
),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['code'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: GestureDetector(
onTap: () {
phoneFormKey.currentState.save();
imageFormKey.currentState.save();
debugPrint(
"_seconds:" +
TimerInheritedWidget.of(innerContext).seconds.toString());
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 48,
color: LabelBlackColor,
alignment: Alignment.center,
child: Text(
'登录/注册',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
),
],
),
);
},
)
);
}
}
5、使用Provider(数据共享方式)
Provider其实就是对InHeritedWidget的封装,用法固定
5.1、Provider用法
- step1: 先添加provider三方库依赖
dependencies:
flutter:
sdk: flutter
provider: ^6.0.0
- step2: 创建一个数据提供者ChangeNotifier。
我们先新建一个 CutdownTimeProvider,继承 ChangeNotifier,使之成为我们的数据提供者之一
cutdown_time_provider.dart
import 'package:flutter/foundation.dart';
class CutdownTimeProvider extends ChangeNotifier {
int _seconds = 0;
int get seconds => _seconds;
void changeSeconds(seconds) {
this._seconds = seconds;
notifyListeners();
}
}
- step3: 页面使用provider数据的地方包裹
在要使用的地方使用ChangeNotifierProvider包裹,并且使用Selector控制是否刷新
ChangeNotifierProvider<CutdownTimeProvider>(
create: (_) => CutdownTimeProvider(),
builder: (context, widget) {
return Selector<CutdownTimeProvider, int>(
selector: (_, v) => v.seconds,
builder: (_, data, child) {
return buildBody(context);
},
);
},
)
Step3: provider具体使用
声明CutdownTimeProvider的provider。
注意:这里的context一定要build声明周期的context
final provider = Provider.of<CutdownTimeProvider>(context, listen: false);
取数据
Text(
provider.seconds == 0 ? '获取验证码' : '${provider.seconds}秒',
style: TextStyle(fontSize: 16),
)
改变数据
至于这里改变为什么会局部刷新,是因为在数据提供者CutdownTimeProvider中调用了notifyListeners();方法
provider.changeSeconds(60);
5.2、页面源码
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'widgets/cutdown_time_provider.dart';
class LoginDemoPage extends StatefulWidget {
LoginDemoPage({Key key});
State<StatefulWidget> createState() {
return _LoginPageState();
}
}
class _LoginPageState extends State<LoginDemoPage> {
bool _loadingImageUrl = true;
String _imageUrl =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAAaCAIAAABw/jbvAAABjElEQVR42s2Y7Q6DIAxF2dxm9v4P7JaYYNeP2wJFZ/ihFS2HlrZQtoGrlDL7W7VbFcq3VLLfl23sGoR0P7c6xCGvJIxwuq8sIx9TUKXv17029Y9Wh3FIwBmBx3buJHw8V9lUfUt57Q0M5basoLkehGdBJ5SQLp4FaRHS+27CiAf1E9K3AJISAkgmiRO6kArh9wMGWR8rofwRI6yKmwirsOJFFnkzIUVSCdWFKgl33YDQGhw1YHfaRLE0eM9+pxJKG7IYiAlTMplOSB2VIlmEch0GCdXpP5WQIamEFE+aRRIyNta/1T/9NGsRqjxMoqYK5n4uoZU50moJa/SuxMJjnE2E0j8TqkI1W1hhykr6QIFKKMMMSxKJVSHyUosQ1DQg0sjGtIAsP7SD6ahrZxDiGHM2YUoAwHuCRKWlo3LPIuze2qcR5mrqhnRL2WQvdXfW6T6cFkubIC804ywv5cc7/034M9rgaU9k1zPjOMs9p3APfkpcx+CKDx4r5TpqdNDHfHhbu+DUgmvQjLLbBy9uOkzsE/1jAAAAAElFTkSuQmCC';
final phoneFormKey = GlobalKey<FormState>();
final imageFormKey = GlobalKey<FormState>();
final codeFormKey = GlobalKey<FormState>();
final Map<String, String> formValue = new HashMap();
Timer _timer;
Color LabelBlackColor = Color(0xFF1A1A1A);
void dispose() {
super.dispose();
if (_timer != null) {
_timer.cancel();
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('登录示例demo'),
),
body: ChangeNotifierProvider<CutdownTimeProvider>(
create: (_) => CutdownTimeProvider(),
builder: (context, widget) {
return Selector<CutdownTimeProvider, int>(
selector: (_, v) => v.seconds,
builder: (_, data, child) {
return buildBody(context);
},
);
},
),
);
}
sendCode(BuildContext context) async {
final provider = Provider.of<CutdownTimeProvider>(context, listen: false);
provider.changeSeconds(60);
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (provider.seconds == 0) {
timer.cancel(); // 取消重复计时
return;
}
provider.changeSeconds(provider.seconds - 1);
});
}
Widget buildBody(BuildContext context) {
final provider = Provider.of<CutdownTimeProvider>(context, listen: false);
return Container(
padding: EdgeInsets.only(right: 20, top: 20),
child: ListView(
children: [
Padding(
padding: EdgeInsets.only(left: 20),
child: Text(
'快捷登录注册',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 26),
),
),
SizedBox(height: 30),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: phoneFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'+86',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
),
hintText: '请输入手机账号',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
),
onSaved: (mobile) {
if (mobile == null || mobile.isEmpty == true) {
return;
}
formValue['mobile'] = mobile;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: codeFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入图形验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_loadingImageUrl) {
return;
} else {
_loadingImageUrl = true;
}
},
child: Container(
width: 100,
height: 30,
child: Image.memory(
base64Decode(_imageUrl
.split(',')[1]
.replaceAll('\r', '')
.replaceAll('\n', '')),
width: 100,
),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['imageCode'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: Form(
key: imageFormKey,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 58, minHeight: 58
// maxWidth: 150,
),
child: TextFormField(
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 20, horizontal: 0),
fillColor: Colors.white,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: LabelBlackColor, width: 1),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xFFF5F5F5),
width: 1.0,
),
),
prefixIcon: Container(
width: 65,
alignment: Alignment.centerLeft,
child: Text(
'验证码',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500),
),
),
hintText: '请输入短信验证码',
hintStyle:
TextStyle(fontSize: 16, color: Color(0xFF999999)),
suffixIcon: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (provider.seconds == 0) {
phoneFormKey.currentState.save();
codeFormKey.currentState.save();
sendCode(context);
}
},
child: Center(
widthFactor: 1,
child: Text(
provider.seconds == 0 ? '获取验证码' : '${provider.seconds}秒',
style: TextStyle(fontSize: 16),
),
),
),
),
onSaved: (text) {
if (text == null || text.isEmpty == true) {
return;
}
formValue['code'] = text;
},
),
),
),
),
SizedBox(height: 20),
Padding(
padding: EdgeInsets.only(left: 20),
child: GestureDetector(
onTap: () {
phoneFormKey.currentState.save();
imageFormKey.currentState.save();
debugPrint("_seconds:" + provider.seconds.toString());
},
behavior: HitTestBehavior.opaque,
child: Container(
height: 48,
color: LabelBlackColor,
alignment: Alignment.center,
child: Text(
'登录/注册',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
),
],
),
);
}
}