在开发Flutter倒计时,setState刷新会造成页面的闪烁,如图
在flutter中常用的刷新方法有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),
),
),
),
),
],
),
);
}
}
此时我们需要仅仅刷新倒计时组件即可,也就是使用局部刷新来解决问题
与React类似,咱们把想刷新的部分抽离成组件,因为React、Flutter的state都是针对整个class的。
这里需要用到父组件调用子组件的发送验证码方法,
首先在子组件中定义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();
}
把刷新相关的功能集成到子组件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),
);
}
}
页面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),
),
),
),
),
],
),
);
}
}
通过抽离组件确实能实现局部刷新,但是书写臃肿。
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),
);
},
)
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),
),
),
),
),
],
),
);
}
}
声明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),
);
},
)
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),
),
),
),
),
],
),
);
}
}
InheritedWidget 组件是功能型组件,提供了沿树向下,共享数据的功能,即子组件可以获取父组件(InheritedWidget 的子类)的数据
InHeritedWidget的使用固定,可用于共享state
首先我们需要创建我们的共享数据类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
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),
),
),
),
),
],
),
);
},
)
);
}
}
Provider其实就是对InHeritedWidget的封装,用法固定
dependencies:
flutter:
sdk: flutter
provider: ^6.0.0
我们先新建一个 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();
}
}
在要使用的地方使用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);
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),
),
),
),
),
],
),
);
}
}
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po
尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub
我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search
由于fast-stemmer的问题,我很难安装我想要的任何rubygem。我把我得到的错误放在下面。Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingfast-stemmer:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcreatingMakefilemake"DESTDIR="cleanmake"DESTDIR=
当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub
我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www
我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。
相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声
首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有, 也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加
SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手