Flutter中SharedPreferences存储的isDone状态无法在UI持久化显示
问题:Todo应用重启/切换页面后已完成任务状态无法正确展示
问题详情
开发的Todo应用支持标记/取消标记任务完成,点击操作时功能正常,isDone值也能保存到SharedPreferences,但重启应用或切换页面后,UI无法展示之前保存的isDone状态——已标记完成的任务会恢复为未标记状态。
相关代码
TodoWidget.dart代码
import 'dart:convert'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:todo/model/todo.dart'; import 'package:todo/provider/todoProvider.dart'; import 'package:todo/widgets/todoEditorWidget.dart'; class TodoWidget extends StatefulWidget { final ToDo todo; const TodoWidget({super.key, required this.todo}); @override State<TodoWidget> createState() => _TodoWidgetState(); } class _TodoWidgetState extends State<TodoWidget> { Color _iconColor = Colors.white; TextDecoration _todoTextDecoration = TextDecoration.none; bool _editButtonVisibility = true; void _markAsDone() async { setState(() { widget.todo.isDone = true; _iconColor = Colors.green; _todoTextDecoration = TextDecoration.lineThrough; _editButtonVisibility = false; }); final prefs = await SharedPreferences.getInstance(); final isDoneKey = 'isDone_${widget.todo.id}'; await prefs.setBool(isDoneKey, true); } void _unmarkAsDone() async { setState(() { widget.todo.isDone = false; _iconColor = Colors.white; _todoTextDecoration = TextDecoration.none; _editButtonVisibility = true; }); final prefs = await SharedPreferences.getInstance(); final isDoneKey = 'isDone_${widget.todo.id}'; await prefs.setBool(isDoneKey, false); } @override Widget build(BuildContext context) { return Card( color: widget.todo.backgroundColor, margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 6), child: Padding( padding: const EdgeInsets.fromLTRB(8, 10, 0, 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Row( children: [ Text( widget.todo.todoTime!, style: const TextStyle( fontSize: 18.0, ), ), const SizedBox(width: 20,), Expanded( child: Text( widget.todo.todoTask!, softWrap: true, style: TextStyle( fontSize: 18.0, decoration: _todoTextDecoration, ), ), ), ], ), ), Row( children: [ IconButton( onPressed: () => widget.todo.isDone ? _unmarkAsDone() : _markAsDone(), icon: Icon(Icons.done, color: _iconColor,), padding: const EdgeInsets.all(5.0), constraints: const BoxConstraints(), tooltip: 'mark as done', ), Visibility( visible: _editButtonVisibility, child: IconButton( onPressed: () => _editTodo(widget.todo), icon: const Icon(CupertinoIcons.pencil), padding: const EdgeInsets.all(5.0), constraints: const BoxConstraints(), tooltip: 'edit task',), ), IconButton( onPressed: _deleteTodo, icon: const Icon(CupertinoIcons.trash), padding: const EdgeInsets.all(5.0), constraints: const BoxConstraints(), tooltip: 'delete task',), ], ), ], ), ), ); } }
TodoProvider中的loadTodos方法
Future<void> loadTodos() async { final prefs = await SharedPreferences.getInstance(); final savedTodos = prefs.getStringList('todos'); if (savedTodos != null) { todosList = savedTodos.map((jsonString) { final todo = ToDo.fromJson(jsonDecode(jsonString)); final isDoneKey = 'isDone_${todo.id}'; todo.isDone = prefs.getBool(isDoneKey) ?? false; // Load the isDone status return todo; }).toList(); } }
首页FutureBuilder代码
FutureBuilder<void>( future: Provider.of<ToDoProvider>(context).loadTodos(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return Consumer<ToDoProvider>( builder: (context, todoProvider, child) { final todosForSelectedDate = todoProvider.getTodosForDate(selectedDate); // Sort the todos list by time in ascending order todosForSelectedDate.sort((a, b) { final timeA = DateTime.parse('2023-10-08 ${a.todoTime}:00'); final timeB = DateTime.parse('2023-10-08 ${b.todoTime}:00'); return timeA.compareTo(timeB); }); if (todosForSelectedDate.isEmpty) { return Column( children: [ const SizedBox(height: 30,), Center( child: Text("No todos today", style: Theme.of(context).textTheme.headlineLarge?.copyWith(fontSize: 25),), ), ], ); } else { return Expanded( child: ListView.builder( itemCount: todosForSelectedDate.length, itemBuilder: (context, index) { return TodoWidget(todo: todosForSelectedDate[index]); }, physics: const BouncingScrollPhysics(), ), ); } }, ); } else if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } else { return const Center(child: Text('Error loading todos')); } }, ),
问题原因
TodoWidget的UI状态变量(_iconColor、_todoTextDecoration、_editButtonVisibility)仅在点击标记/取消按钮时通过setState更新,但初始化时没有根据widget.todo.isDone的实际值来设置这些变量。
当应用重启或切换页面后,虽然todo.isDone已经从SharedPreferences加载正确值,但UI状态还是保持初始的未完成状态(白色图标、无删除线、显示编辑按钮),导致视觉上显示错误。
解决方案
在_TodoWidgetState中添加初始化逻辑,根据传入的widget.todo.isDone值设置UI状态,同时监听widget更新(比如切换页面回来后todo数据变化),更新UI状态:
修改后的_TodoWidgetState代码
class _TodoWidgetState extends State<TodoWidget> { Color _iconColor = Colors.white; TextDecoration _todoTextDecoration = TextDecoration.none; bool _editButtonVisibility = true; @override void initState() { super.initState(); // 初始化时根据todo的isDone设置UI状态 _updateUIState(widget.todo.isDone); } @override void didUpdateWidget(covariant TodoWidget oldWidget) { super.didUpdateWidget(oldWidget); // 当todo数据更新时(比如切换页面后重新加载),更新UI状态 if (oldWidget.todo.isDone != widget.todo.isDone) { _updateUIState(widget.todo.isDone); } } // 提取统一的UI状态更新方法 void _updateUIState(bool isDone) { setState(() { _iconColor = isDone ? Colors.green : Colors.white; _todoTextDecoration = isDone ? TextDecoration.lineThrough : TextDecoration.none; _editButtonVisibility = !isDone; }); } void _markAsDone() async { setState(() { widget.todo.isDone = true; _updateUIState(true); }); final prefs = await SharedPreferences.getInstance(); final isDoneKey = 'isDone_${widget.todo.id}'; await prefs.setBool(isDoneKey, true); } void _unmarkAsDone() async { setState(() { widget.todo.isDone = false; _updateUIState(false); }); final prefs = await SharedPreferences.getInstance(); final isDoneKey = 'isDone_${widget.todo.id}'; await prefs.setBool(isDoneKey, false); } // ... 原有的build方法和其他代码保持不变 }
说明
initState:在组件初始化时,根据传入的todo.isDone值设置UI状态,确保加载时UI展示正确。didUpdateWidget:当widget的todo属性发生变化时(比如切换页面后重新获取todo数据),对比新旧todo的isDone值,不同则更新UI状态。_updateUIState:提取统一的状态更新逻辑,避免重复代码,让标记/取消操作和初始化/更新操作共用同一逻辑。
这样修改后,重启应用或切换页面时,UI会根据加载后的todo.isDone值正确展示完成状态。
内容的提问来源于stack exchange,提问作者eqrakhattak
相关产品推荐
相关产品推荐

