Flutter实现TextButton点击后API请求时显示加载状态
实现登录按钮加载状态及跳转功能
修改步骤:
- 添加
_isLoading状态变量控制加载状态 - 根据加载状态切换按钮显示内容(加载时显示
CircularProgressIndicator) - 请求前后更新加载状态,避免重复点击
修改后的完整代码:
import "dart:convert"; import 'package:http/http.dart' as http; import 'register.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() => runApp(const MaterialApp( home: LoginScreen(), )); class LoginScreen extends StatefulWidget { const LoginScreen({Key? key}) : super(key: key); @override State<LoginScreen> createState() => _LoginScreenState(); } class _LoginScreenState extends State<LoginScreen> { bool passwordVisible = false; late String email; late String password1; var md5password = ""; late SharedPreferences prefs; bool _isLoading = false; // 新增:控制加载状态 @override void initState() { super.initState(); initSharedPref(); passwordVisible = true; } void initSharedPref() async { prefs = await SharedPreferences.getInstance(); } String generateMd5(String input) { return md5.convert(utf8.encode(input)).toString(); } GlobalKey<FormState> formKey = GlobalKey<FormState>(); final emailController = TextEditingController(); final passwordController = TextEditingController(); @override void dispose() { super.dispose(); emailController.dispose(); passwordController.dispose(); } @override Widget build(BuildContext context) { double height = MediaQuery.of(context).size.height; double width = MediaQuery.of(context).size.width; return WillPopScope( onWillPop: () async { return false; }, child: Scaffold( backgroundColor: Colors.white, body: SingleChildScrollView( child: Form( key: formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: height * 0.30, width: double.infinity, decoration: const BoxDecoration(color: Colors.white), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ Image.asset( "assets/title.png", width: 350.0, ), const SizedBox( height: 30, ), ], ), ), const SizedBox( height: 50, ), const Center( child: Text('Login', style: TextStyle( fontSize: 30.0, fontWeight: FontWeight.bold, color: Colors.black)), ), const SizedBox(height: 30), Center( child: SizedBox( height: height * 0.08, width: width - 35, child: TextFormField( controller: emailController, validator: (email) { if (email == null || email.isEmpty) { return "Please enter Email"; } else if (!email.contains("@") || !email.contains(".")) { return "Please enter a valid email address"; } return null; }, onChanged: (value) { setState(() { email = value; }); }, decoration: const InputDecoration( border: OutlineInputBorder(), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.black)), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Color(0xFF006ff6)), ), prefixIcon: Icon(Icons.email, color: Color(0xFF006ff6)), labelText: "Email", labelStyle: TextStyle(color: Colors.grey, fontSize: 13.0), ), ), ), ), const SizedBox(height: 30), Center( child: SizedBox( height: height * 0.08, width: width - 35, child: TextFormField( controller: passwordController, validator: (password) { if (password == null || password.isEmpty) { return "Please enter your password"; } return null; }, onChanged: (value) { setState(() { password1 = value; }); md5password = generateMd5(password1); print(md5password); }, obscureText: passwordVisible, obscuringCharacter: "*", decoration: InputDecoration( border: const OutlineInputBorder(), enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.black)), focusedBorder: const OutlineInputBorder( borderSide: BorderSide(color: Color(0xFF006ff6)), ), prefixIcon: const Icon(Icons.lock_open, color: Color(0xFF006ff6)), labelText: "Password", labelStyle: const TextStyle(color: Colors.grey, fontSize: 13.0), suffixIcon: IconButton( icon: Icon(passwordVisible ? Icons.visibility : Icons.visibility_off), onPressed: () { setState(() { passwordVisible = !passwordVisible; }); }, ), ), ), ), ), Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const ForgotPass())); }, child: const Text( "Forgot your Password ?", style: TextStyle( color: Colors.grey, decoration: TextDecoration.underline), ), ), ), const SizedBox(height: 20), Center( child: SizedBox( height: height * 0.08, width: width - 35, child: TextButton( onPressed: _isLoading ? null // 加载时禁用按钮 : () async { if (formKey.currentState!.validate()) { print(email); print(md5password); await loginUser(); } }, style: TextButton.styleFrom( backgroundColor: const Color(0xFF006ff6), shadowColor: Colors.black, elevation: 5, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10)))), child: _isLoading ? const CircularProgressIndicator( color: Colors.white, ) : const Text( "SIGN IN", style: TextStyle( color: Colors.white, fontSize: 20.0, fontWeight: FontWeight.bold, ), ), ), ), ), const SizedBox( height: 20.0, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( "Don't have an account?", style: TextStyle(color: Colors.grey), ), TextButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const Register())); }, child: const Text( "Sign up", style: TextStyle(color: Colors.black), )) ], ) ], ), ), ), ), ); } Future<void> loginUser() async { setState(() { _isLoading = true; // 请求开始,设置加载状态 }); try { if (emailController.text.isNotEmpty && passwordController.text.isNotEmpty) { var reqBody = { "email": emailController.text, "password": generateMd5(passwordController.text) }; var response = await http.post( Uri.parse("https://testing.com/user/login"), headers: {"Content-Type": "application/json"}, body: jsonEncode(reqBody)); if (response.statusCode == 200) { var jsonResponse = jsonDecode(response.body); if (jsonResponse['success']) { var myToken = jsonResponse['data']['token']; await prefs.setString('token', myToken); print(myToken); if (mounted) { // 确保widget未被销毁再导航 Navigator.push( context, MaterialPageRoute( builder: (context) => Dashboard(token: myToken))); } } else { print('Something went wrong'); } print(response.statusCode); } else if (response.statusCode == 400) { if (mounted) { const snackBar = SnackBar( content: Text('Incorrect Username or Password'), backgroundColor: Colors.red, elevation: 10, behavior: SnackBarBehavior.floating, margin: EdgeInsets.all(5), ); ScaffoldMessenger.of(context).showSnackBar(snackBar); } } else if (response.statusCode == 500) { if (mounted) { const snackBar = SnackBar( content: Text('Server Error'), backgroundColor: Colors.red, elevation: 10, behavior: SnackBarBehavior.floating, margin: EdgeInsets.all(5), ); ScaffoldMessenger.of(context).showSnackBar(snackBar); } } else { if (mounted) { const snackBar = SnackBar( content: Text('Something went wrong'), backgroundColor: Colors.red, elevation: 10, behavior: SnackBarBehavior.floating, margin: EdgeInsets.all(5), ); ScaffoldMessenger.of(context).showSnackBar(snackBar); } } } } catch (e) { print("Error: $e"); if (mounted) { const snackBar = SnackBar( content: Text('Network error occurred'), backgroundColor: Colors.red, elevation: 10, behavior: SnackBarBehavior.floating, margin: EdgeInsets.all(5), ); ScaffoldMessenger.of(context).showSnackBar(snackBar); } } finally { if (mounted) { setState(() { _isLoading = false; // 请求结束,取消加载状态 }); } } } } // 补充原代码缺失的类定义 class ForgotPass extends StatelessWidget { const ForgotPass({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Forgot Password")), ); } } class Dashboard extends StatelessWidget { final String token; const Dashboard({Key? key, required this.token}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Dashboard")), body: Center(child: Text("Token: $token")), ); } }
关键修改说明:
- 加载状态控制:新增
_isLoading变量标记请求状态,加载时禁用按钮避免重复提交 - UI动态切换:按钮根据
_isLoading状态显示加载指示器或登录文本 - 状态安全更新:在
finally块中统一恢复加载状态,确保无论请求成功/失败/异常都能重置UI - 安全导航:使用
mounted检查widget存活状态,避免异步操作后导航报错 - 代码修复:补充原代码缺失的导入、未定义变量及类定义,保证代码可运行
内容的提问来源于stack exchange,提问作者Sakthivel k
相关产品推荐
相关产品推荐

