You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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方法和其他代码保持不变
}

说明

  1. initState:在组件初始化时,根据传入的todo.isDone值设置UI状态,确保加载时UI展示正确。
  2. didUpdateWidget:当widget的todo属性发生变化时(比如切换页面后重新获取todo数据),对比新旧todo的isDone值,不同则更新UI状态。
  3. _updateUIState:提取统一的状态更新逻辑,避免重复代码,让标记/取消操作和初始化/更新操作共用同一逻辑。

这样修改后,重启应用或切换页面时,UI会根据加载后的todo.isDone值正确展示完成状态。

内容的提问来源于stack exchange,提问作者eqrakhattak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 08:42:05