如何修复Flutter SharedPreferences的「Bad State: No element」错误及选词持久化
问题描述
现有Flutter代码从Firebase获取词汇,支持用户选择/取消选择词汇(选中项变色),并将状态同步保存到Firebase。现在需要添加SharedPreferences实现状态持久化:用户选择词汇点击「next」后,关闭重启App能保留选中/未选中状态,且仅选中项着色。但当前使用SharedPreferences时出现**「Bad State: No element」**错误,需解决该问题并完成功能。
错误原因
- 空列表调用
first方法:initState中直接执行dropdownValueselectedWord = selectedWord.first,但selectedWord初始是空列表,调用first会触发「No element」错误。 - SharedPreferences存储逻辑错误:
- 使用相同的key(
SelectedWordObject)保存选中和未选中词汇,导致数据覆盖。 - 用
setString存储列表数据,SharedPreferences无法直接存储列表,需用setStringList。
- 使用相同的key(
- 初始化顺序错误:先执行取列表
first的操作,再读取SharedPreferences数据,导致初始状态错误。
修复方案与完整代码
以下是修复后的完整代码,解决了上述问题并实现状态持久化:
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hexcolor/hexcolor.dart'; import 'package:shared_preferences/shared_preferences.dart'; // 请确保导入你的Config、SignInProvider和HomeScreen import 'your_import_path.dart'; class uitry extends StatefulWidget { const uitry({Key? key}) : super(key: key); @override State<uitry> createState() => _uitryState(); } class _uitryState extends State<uitry> { List<Words12> wordList = []; List<String> selectedWord = []; List<String> deSelectedWord = []; // 从Firebase获取词汇列表 Future<List<Words12>> fetchRecords() async { var records = await FirebaseFirestore.instance .collection('12words') .where("categoryName", isEqualTo: "Objects12") .get(); return mapRecords(records); } List<Words12> mapRecords(QuerySnapshot<Map<String, dynamic>> records) { return records.docs.map((data) => Words12.fromJson(data.data())).toList(); } @override void initState() { super.initState(); _loadSavedState(); // 优先加载本地保存的状态 } // 从SharedPreferences加载保存的选中/未选中状态 Future<void> _loadSavedState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); // 读取选中列表,为空则初始化空列表 List<String>? savedSelected = prefs.getStringList("selected_words_objects"); // 读取未选中列表,为空则后续从Firebase数据初始化 List<String>? savedDeSelected = prefs.getStringList("deselected_words_objects"); setState(() { selectedWord = savedSelected ?? []; deSelectedWord = savedDeSelected ?? []; }); // 如果未选中列表为空,首次加载时将所有Firebase词汇设为未选中(除了已选中的) if (savedDeSelected == null) { List<Words12> words = await fetchRecords(); setState(() { deSelectedWord = words .map((word) => word.wordName) .where((name) => !selectedWord.contains(name)) .toList(); }); } } // 保存选中/未选中状态到SharedPreferences Future<void> _saveState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setStringList("selected_words_objects", selectedWord); await prefs.setStringList("deselected_words_objects", deSelectedWord); } @override Widget build(BuildContext context) { double height = MediaQuery.of(context).size.height; double width = MediaQuery.of(context).size.width; return Scaffold( body: Container( decoration: const BoxDecoration( image: DecorationImage( image: AssetImage(Config.app_background4), fit: BoxFit.fill), ), child: SafeArea( child: Center( child: SingleChildScrollView( child: Column( children: <Widget>[ Row( children: <Widget>[ Expanded( child: ListTile( leading: GestureDetector( child: const Icon( Icons.arrow_back_ios_new_sharp, color: Colors.black, size: 24.0, ), onTap: () => Navigator.pop(context), ), title: const Padding( padding: EdgeInsets.only(top: 32, right: 35), child: Text( "Under 18 months", textAlign: TextAlign.center, style: TextStyle( color: Colors.black, fontSize: 18.00, fontWeight: FontWeight.w700, ), ), ), ), ), ], ), const SizedBox(height: 0), Padding( padding: const EdgeInsets.only(top: 1, right: 0), child: Column( children: [ Material( color: HexColor('#E92F54').withOpacity(0.9), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(0).copyWith( topLeft: const Radius.circular(28.0), topRight: const Radius.circular(28.0), ), ), child: SizedBox( width: width * 0.94, height: height * 0.062, child: const Column( children: [ SizedBox(height: 6.5), Text('Understanding', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 16.0)), Text('Object', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 15.0)) ], ), ), ), Material( color: HexColor('#FFFBFB').withOpacity(0.7), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(2).copyWith( bottomLeft: const Radius.circular(28.0), bottomRight: const Radius.circular(28.0), ), ), child: SizedBox( width: width * 0.94, height: height * 0.30, child: Column( children: [ SizedBox( height: height * 0.18, child: SingleChildScrollView( child: Column( children: const [SizedBox(height: 10)], ), ), ), Expanded( child: FutureBuilder<List<Words12>>( future: fetchRecords(), builder: (context, snapshot) { if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } else { wordList = snapshot.data!; // 如果未选中列表为空(首次加载),初始化未选中列表 if (deSelectedWord.isEmpty && selectedWord.isEmpty) { deSelectedWord = wordList.map((word) => word.wordName).toList(); } return SingleChildScrollView( child: Wrap( children: wordList.map((word) { bool isSelected = selectedWord.contains(word.wordName); return GestureDetector( onTap: () { setState(() { if (!isSelected) { if (selectedWord.length < 50) { selectedWord.add(word.wordName); deSelectedWord.remove(word.wordName); } } else { selectedWord.remove(word.wordName); deSelectedWord.add(word.wordName); } }); }, child: Container( margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 4), child: Container( padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 12), decoration: BoxDecoration( color: isSelected ? HexColor('#3A97FF') : HexColor('#D9D9D9'), borderRadius: BorderRadius.circular(18), border: Border.all( color: isSelected ? HexColor('#3A97FF') : HexColor('#D9D9D9'), width: 1)), child: Text( word.wordName, style: const TextStyle( color: Colors.black, fontSize: 14, fontWeight: FontWeight.w700), ), ), ), ); }).toList(), ), ); } }), ), ], ), ), ), ], ), ), const SizedBox(height: 25), Center( child: SizedBox( width: 160.0, height: 35.0, child: ElevatedButton( style: ButtonStyle( shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(18.0), side: const BorderSide(color: Colors.blueAccent), ), ), ), onPressed: displayMessage, child: const Text("next"), ), ), ), ], ), ))), ), ); } void displayMessage() async { // 先保存状态到SharedPreferences await _saveState(); // 同步到Firebase final sp = context.read<SignInProvider>(); await FirebaseFirestore.instance.collection("objects").doc(sp.uid).set({ "speackSE": selectedWord, "speackUN": deSelectedWord, }); // 跳转到首页 Navigator.push( context, MaterialPageRoute(builder: (context) => const HomeScreen()), ); } } // 请确保Words12类已正确实现 class Words12 { final String wordName; final String categoryName; Words12({required this.wordName, required this.categoryName}); factory Words12.fromJson(Map<String, dynamic> json) { return Words12( wordName: json['wordName'], categoryName: json['categoryName'], ); } }
关键修复点说明
- 移除空列表
first调用:删除initState中直接取selectedWord.first的代码,改为从SharedPreferences加载数据后初始化状态。 - SharedPreferences列表存储:使用
setStringList和getStringList来保存和读取选中/未选中词汇列表,分别使用不同的key避免数据覆盖。 - 初始化逻辑调整:首次加载时,如果未保存过状态,将Firebase返回的所有词汇设为未选中;如果有保存的状态,则直接恢复。
- 状态保存时机:点击「next」按钮时,先保存状态到SharedPreferences,再同步到Firebase,确保本地持久化优先完成。
- UI状态更新:在加载本地数据和点击词汇时调用
setState,确保UI实时更新选中状态和颜色。
内容的提问来源于stack exchange,提问作者Dasun Dola
相关产品推荐
相关产品推荐

