Flutter按钮需双击触发API的Provider状态管理异常问题
Flutter Provider状态管理异常问题
问题现象
- 首次点击登录按钮无任何响应,第二次点击才会调用接口获取数据
- 输入正确凭证登录成功后,立即输入错误凭证,应用仍提示登录成功,状态未更新
前端代码
class _LoginPageState extends State<LoginPage> { String? myError; @override Widget build(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; TextEditingController userNameController = TextEditingController(); TextEditingController passwordController = TextEditingController(); return Directionality( textDirection: TextDirection.rtl, child: Consumer<PicoProvider>( builder: (context, prov, child) => SafeArea( child: Scaffold( backgroundColor: CustomColors.scaffoldDark, body: Padding( padding: const EdgeInsets.all(100), child: SingleChildScrollView( child: Container( width: screenWidth, child: Form( autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.5), spreadRadius: 5, blurRadius: 7, offset: const Offset(0, 3), ), ]), child: Image.asset( 'assets/images/main_logo.jpeg', height: 300, ), ), const SizedBox(height: 20), CTextField( label: 'اسم المسخدم', icon: Ionicons.person, isObsecured: false, controller: userNameController), const SizedBox(height: 20), CTextField( label: 'كلمة المرور', icon: Ionicons.lock_closed, isObsecured: true, controller: passwordController, onSubmitted: () { prov.websiteLogin(userNameController.text, passwordController.text); }), ElevatedButton( onPressed: () { prov.websiteLogin(userNameController.text, passwordController.text); prov.errorText!.isNotEmpty ? giveMeDialog( context, 'hello', 'not found', 'error') : giveMeDialog(context, 'hello', 'you are logged', 'success'); }, child: const Text('Click')), const SizedBox(height: 20), prov.isLoading ? const CircularProgressIndicator() : const SizedBox(), Text('${prov.errorText}') ], ), ), ), ), ), ), ), ), ); } Future<void> giveMeDialog( BuildContext context, String title, String message, String icon) { return showDialog( context: context, builder: (context) { return AlertDialog( title: Text(title), content: Directionality( textDirection: TextDirection.rtl, child: Column( children: [ Text( message, style: const TextStyle(fontSize: 30), ), Lottie.asset('assets/animations/${icon}_animation.json'), ], ), ), ); }, ); } }
后端代码
class PicoProvider extends ChangeNotifier { String baseURL = 'http://111.11.11.111:1111'; bool _isLoading = false; bool get isLoading => _isLoading; User? _user = User(); User? get user => _user; String? _errorText; String? get errorText => _errorText; Future<void> websiteLogin(String userName, String password) async { try { String endPoint = '$baseURL/PicoLogin?UserName=$userName&Password=$password'; _isLoading = true; notifyListeners(); var response = await get(Uri.parse(endPoint), headers: { "content-type": "application/json", "Access-Control-Allow-Origin": "*", }); if (response.statusCode == 200) { _user = User.fromJson(jsonDecode(response.body)); _errorText = ''; } else { _errorText = 'User Not Found'; } _isLoading = false; notifyListeners(); } catch (e) { _errorText = e.toString(); notifyListeners(); print('Error: ${e.toString()}'); } } }
问题原因与修复方案
问题1:首次点击无响应
原因:TextEditingController在build方法内部初始化,每次组件重建时都会生成新的控制器实例,导致输入框无法与稳定的控制器绑定,首次点击时无法获取有效输入值,接口调用无实际参数,表现为无响应。
修复:将控制器声明为State类的成员变量,在initState中初始化,并在dispose中释放资源,确保控制器生命周期与页面一致。
问题2:状态未更新导致错误提示异常
原因:登录按钮的onPressed中,调用异步方法websiteLogin后立刻同步判断errorText,此时接口请求尚未完成,使用的是请求前的旧状态值,因此错误凭证会沿用之前成功的状态提示。
修复:等待websiteLogin异步方法执行完成后,再根据最新的状态值判断并显示弹窗,确保使用的是接口请求后的最新状态。
修复后的前端代码
class _LoginPageState extends State<LoginPage> { String? myError; late TextEditingController userNameController; late TextEditingController passwordController; @override void initState() { super.initState(); // 初始化控制器,确保实例唯一 userNameController = TextEditingController(); passwordController = TextEditingController(); } @override void dispose() { // 页面销毁时释放控制器资源 userNameController.dispose(); passwordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; return Directionality( textDirection: TextDirection.rtl, child: Consumer<PicoProvider>( builder: (context, prov, child) => SafeArea( child: Scaffold( backgroundColor: CustomColors.scaffoldDark, body: Padding( padding: const EdgeInsets.all(100), child: SingleChildScrollView( child: Container( width: screenWidth, child: Form( autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.5), spreadRadius: 5, blurRadius: 7, offset: const Offset(0, 3), ), ]), child: Image.asset( 'assets/images/main_logo.jpeg', height: 300, ), ), const SizedBox(height: 20), CTextField( label: 'اسم المسخدم', icon: Ionicons.person, isObsecured: false, controller: userNameController), const SizedBox(height: 20), CTextField( label: 'كلمة المرور', icon: Ionicons.lock_closed, isObsecured: true, controller: passwordController, onSubmitted: () async { await prov.websiteLogin(userNameController.text, passwordController.text); _showLoginResultDialog(context, prov); }), ElevatedButton( onPressed: () async { // 等待异步登录方法完成 await prov.websiteLogin(userNameController.text, passwordController.text); // 根据最新状态显示弹窗 _showLoginResultDialog(context, prov); }, child: const Text('Click')), const SizedBox(height: 20), prov.isLoading ? const CircularProgressIndicator() : const SizedBox(), Text('${prov.errorText}') ], ), ), ), ), ), ), ), ), ); } // 提取弹窗逻辑,复用代码 void _showLoginResultDialog(BuildContext context, PicoProvider prov) { if (prov.errorText?.isNotEmpty ?? false) { giveMeDialog(context, 'hello', 'not found', 'error'); } else { giveMeDialog(context, 'hello', 'you are logged', 'success'); } } Future<void> giveMeDialog( BuildContext context, String title, String message, String icon) { return showDialog( context: context, builder: (context) { return AlertDialog( title: Text(title), content: Directionality( textDirection: TextDirection.rtl, child: Column( // 避免弹窗高度过大,设置mainAxisSize为min mainAxisSize: MainAxisSize.min, children: [ Text( message, style: const TextStyle(fontSize: 30), ), Lottie.asset('assets/animations/${icon}_animation.json'), ], ), ), ); }, ); } }
内容的提问来源于stack exchange,提问作者Mohammad Radwan
相关产品推荐
相关产品推荐

