Flutter中Appwrite创建会话随机抛出Concurrent Modification异常
问题原因
- 核心错误:每次调用登录方法时都重新初始化
Client和Account实例。Appwrite SDK内部维护了请求拦截器、响应回调、状态监听器的可增长列表,重复创建的多个Client实例在异步请求返回时,会同时对监听器列表做增删操作,Dart异步模型在迭代列表过程中如果检测到列表被修改,就会抛出Concurrent modification during iteration异常,因为异步返回时机完全随机,所以故障没有明显规律。 - 次要问题1:将
TextEditingController定义在StatelessWidget的build方法中,每次页面重建都会生成新的控制器,触发不必要的组件重建,放大了多实例带来的并发风险。 - 次要问题2:没有做请求防重复处理,用户快速多次点击登录按钮时,会同时触发多个登录请求、生成多组Client实例,进一步提升并发修改的触发概率。
- 次要问题3:异步回调中执行导航操作前没有校验组件挂载状态,页面提前销毁时可能触发SDK回调链路的异常中断,意外修改内部迭代中的列表。
解决方案
1. 全局单例初始化Appwrite实例
新建独立的服务管理文件,全局只初始化一次Client和相关服务实例,所有接口调用复用同一个实例,从根源避免多实例并发修改内部列表的问题。
// appwrite_service.dart import 'package:appwrite/appwrite.dart'; class AppwriteService { static final AppwriteService _instance = AppwriteService._internal(); late final Client client; late final Account account; factory AppwriteService() => _instance; AppwriteService._internal() { client = Client() .setEndpoint('https://jeff.vmsimple.com/v1') .setProject('jeffs-timekeeper'); account = Account(client); // 其他Appwrite服务(Databases、Storage等)也在这里统一初始化 } }
2. 重构登录页为StatefulWidget
将文本控制器移到State生命周期中管理,避免build方法重复生成新控制器,同时增加登录防重复逻辑、组件挂载状态校验。
class LoginScreen extends StatefulWidget { const LoginScreen({Key? key, required this.title}) : super(key: key); final String title; @override State<LoginScreen> createState() => _LoginScreenState(); } class _LoginScreenState extends State<LoginScreen> { late final TextEditingController _emailController; late final TextEditingController _passwordController; bool _isLoginLoading = false; @override void initState() { super.initState(); _emailController = TextEditingController(); _passwordController = TextEditingController(); } @override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); } Future<void> _handleLogin() async { // 拦截重复点击 if (_isLoginLoading) return; setState(() => _isLoginLoading = true); try { // 复用全局单例的Account实例 final response = await AppwriteService().account.createSession( email: _emailController.text.trim(), password: _passwordController.text, ); print(response); // 页面销毁后不再执行导航操作 if (!mounted) return; Navigator.popAndPushNamed(context, '/dashboardScreen'); } catch (error) { print(error); if (!mounted) return; // 可按需增加错误提示 ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('登录失败,请检查账号密码')), ); } finally { if (mounted) { setState(() => _isLoginLoading = false); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'Login', style: Theme.of(context).textTheme.headline4, ), Container( margin: const EdgeInsets.fromLTRB(25, 25, 25, 12.5), child: TextField( controller: _emailController, obscureText: false, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Username', ), ), ), Container( margin: const EdgeInsets.fromLTRB(25, 12.5, 25, 25), child: TextField( controller: _passwordController, obscureText: true, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Password', ), ), ), Container( margin: const EdgeInsets.only(bottom: 25), child: Row( children: [ Expanded( child: Column( children: [ ElevatedButton( // 加载状态下禁用按钮点击 onPressed: _isLoginLoading ? null : _handleLogin, child: _isLoginLoading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text('Login'), ) ], ), ), ], ), ) ], ), ), ); } }
3. 其他注意事项
- 项目中所有Appwrite接口调用,都必须复用全局单例的Client实例初始化对应服务,禁止在接口方法中临时新建Client。
- 所有涉及页面跳转、状态更新的异步回调,执行前必须校验
mounted属性,避免组件销毁后执行上下文相关操作触发SDK链路异常。
内容的提问来源于stack exchange,提问作者Saphiric
相关产品推荐
相关产品推荐

