Flutter - 每次关闭应用程序时存储对象列表的最佳方法是什么?

6

情况:
我对Flutter和移动开发非常陌生,因此对Dart不是很了解。我已经阅读了一些与我类似问题的人提出的解决方案,但没有成功将这些解决方案应用到我的项目中。

问题:
我有一个待办事项应用程序,其中有两个对象列表,我希望在用户重新打开应用程序时存储这些列表。

我知道这是简单的东西,但由于缺乏经验,我感觉自己正在为这个问题而奋斗...因此,我决定来寻求一些帮助。

我尝试过的方法:
我已经找到了不同的解决方案,但所有这些解决方案对于这种情况都显得过于复杂(与我保存列表到归档时习惯的做法相比),包括:将列表编码为映射并在使用SharedPreferences之前转换为字符串,使用SQlite数据库(我看过的每个教程都让我感觉像是用战争坦克杀掉一只蚂蚁,我对Firebase也持同样的看法)。

问题结构:
待办事项屏幕上有一个ListView.builder,调用2个数组:正在进行的任务和已完成的任务,我希望在用户进行更改时将它们写入手机。我不知道是否应该仅尝试通过调用一些包方法从它们所属的类中保存这些数组,还是如果可能的话,尝试存储整个应用程序

结论:
是否有简单的方法解决这个问题?或者我应该使用像Firebase这样强大的东西吗?即使我没有习惯使用Firestore,因此我不知道如何将这种东西应用于保存数据。

我的列表结构:

List<Task> _tasks = [
    Task(
      name: "do something",
      description: "try to do it fast!!!",
    ),
  ];

List<Task> _doneTasks = [
 Task(
      name: "task marked as done",
      description: "something",
    ),
];

@Loren.A 感谢您的帮助!我成功地使用了您提供的 provider 和 get_storage 逻辑。我想知道如何从存储列表中删除一个元素。我尝试使用 storageList.remove('tasks'),但什么也没有发生;当我尝试调用 object box.remove('tasks') 时,应用程序崩溃了 - NoSuchMethodError:在 restoreTasks 的循环中调用了 null 上的 getter 'length'。 - Louis
2个回答

5

使用空安全进行2022年更新

我的原始代码示例比必要的更冗长。使用Dart的factory构造函数可以用较少的代码完成此操作。同时,这也已更新为使用空安全和Hive而非GetStorage。

首先,添加一个toMap方法,将对象转换为Map,然后添加一个fromMap构造函数,从存储中保存的Map返回一个Task对象。

class Task {
  final String name;
  final String description;

  Task({required this.name, required this.description});

  Map<String, dynamic> toMap() {
    return {'name': this.name, 'description': this.description};
  }

  factory Task.fromMap(Map map) {
    return Task(
      name: map['name'],
      description: map['description'],
    );
  }

  String toString() {
    return 'name: $name description: $description';
  }
}

更新的演示页面


class StorageDemo extends StatefulWidget {
  @override
  _StorageDemoState createState() => _StorageDemoState();
}

class _StorageDemoState extends State<StorageDemo> {
  List<Task> _tasks = [];

  final box = Hive.box('taskBox');

  // separate list for storing maps/ restoreTask function
  //populates _tasks from this list on initState

  List storageList = [];

  void addAndStoreTask(Task task) {
    _tasks.add(task);

    storageList.add(task.toMap()); // adding temp map to storageList
    box.put('tasks', storageList); // adding list of maps to storage
  }

  void restoreTasks() {
    storageList = box.get('tasks') ?? []; // initializing list from storage

// looping through the storage list to parse out Task objects from maps
    for (final map in storageList) {
      _tasks
          .add(Task.fromMap(map)); // adding Tasks back to your normal Task list
    }
  }

// looping through your list to see whats inside
  void printTasks() {
    for (final task in _tasks) {
      log(task.toString());
    }
  }

  void clearTasks() {
    _tasks.clear();
    storageList.clear();
    box.clear();
  }

  @override
  void initState() {
    super.initState();
    restoreTasks(); // restore list from storing in initState
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: Container(),
          ),
          TextButton(
            onPressed: () {
              final task =
                  Task(description: 'test description', name: 'test name');
              addAndStoreTask(task);
            },
            child: Text('Add Task'),
          ),
          TextButton(
            onPressed: () {
              printTasks();
            },
            child: Text('Print Storage'),
          ),
          TextButton(
            onPressed: () {
              clearTasks();
            },
            child: Text('Clear Tasks'),
          ),
        ],
      ),
    );
  }
}

更新存储初始化

void main() async {
  await Hive.initFlutter();
  await Hive.openBox('taskBox');
  runApp(MyApp());
}

原始答案

一般来说,一旦您想要存储除了基本类型(例如 String int 等)之外的任何内容,情况就会变得更加复杂,因为它们必须转换为任何存储解决方案都可以读取的东西。

所以尽管 Tasks 是一个只有几个字符串的基本对象,但是 SharedPreferences 或其他任何东西都不知道 Task 是什么或该怎么处理它。

我建议一般来说阅读有关 JSON 序列化的文章,因为无论如何您都需要了解它。 这是一个好的开始这是另一篇好的文章

尽管如此,也可以通过将任务转换为 Map(这正是 json 序列化所做的)并将其存储到映射列表中来完成,而不使用 json。我将向您展示手动执行此操作而不使用 json 的示例。但是再次强调,花些时间学习它对您非常有利。

此示例将使用 Get Storage,它类似于 SharedPreferences,但更容易使用,因为您不需要为不同的数据类型编写不同的方法,只需要使用 readwrite 即可。

我不知道您在应用程序中如何添加任务,但这只是存储 Task 对象列表的基本示例。任何不涉及在线存储的解决方案都需要在本地进行存储,并在应用程序启动时从存储中检索。

所以假设这里是您的 Task 对象。

class Task {
  final String name;
  final String description;

  Task({this.name, this.description});
}

将此代码放在主方法中,在运行应用程序之前。
await GetStorage.init();

你需要在你的主函数中添加async,如果你不熟悉它的工作原理,它看起来像这样。
void main() async {
  await GetStorage.init();

  runApp(MyApp());
}

通常情况下,我不会在有状态的小部件中执行所有这些逻辑,而是实现一种状态管理解决方案并将其放在 UI 之外的一个类中,但这是完全不同的讨论。我也建议您查看 GetX、Riverpod 或 Provider,并阅读它们的介绍,并选择其中最容易学习的一个。对于简单和功能而言,我推荐 GetX。
但是,由于您刚开始学习,我将忽略此部分内容,暂时将所有这些函数放在 UI 页面中。
另外,不仅可以在应用关闭时存储,还可以在列表更改时随时存储,这样更容易。
这里有一个页面,带有一些按钮,可添加、清除和打印存储,以便在应用重新启动后查看列表中确切的内容。
如果您理解这里正在发生的事情,那么您应该能够在您的应用程序中做到这一点,或者学习 JSON 并以那种方式完成它。无论哪种方法,您都需要了解如何使用任何可用解决方案中的 Maps 和本地存储。
class StorageDemo extends StatefulWidget {
  @override
  _StorageDemoState createState() => _StorageDemoState();
}

class _StorageDemoState extends State<StorageDemo> {
  List<Task> _tasks = [];

  final box = GetStorage(); // list of maps gets stored here

  // separate list for storing maps/ restoreTask function
  //populates _tasks from this list on initState

  List storageList = [];

  void addAndStoreTask(Task task) {
    _tasks.add(task);

    final storageMap = {}; // temporary map that gets added to storage
    final index = _tasks.length; // for unique map keys
    final nameKey = 'name$index';
    final descriptionKey = 'description$index';

// adding task properties to temporary map

    storageMap[nameKey] = task.name;
    storageMap[descriptionKey] = task.description;

    storageList.add(storageMap); // adding temp map to storageList
    box.write('tasks', storageList); // adding list of maps to storage
  }

  void restoreTasks() {
    storageList = box.read('tasks'); // initializing list from storage
    String nameKey, descriptionKey;

// looping through the storage list to parse out Task objects from maps
    for (int i = 0; i < storageList.length; i++) {
      final map = storageList[i];
      // index for retreival keys accounting for index starting at 0
      final index = i + 1;

      nameKey = 'name$index';
      descriptionKey = 'description$index';

      // recreating Task objects from storage

      final task = Task(name: map[nameKey], description: map[descriptionKey]);

      _tasks.add(task); // adding Tasks back to your normal Task list
    }
  }

// looping through you list to see whats inside
  void printTasks() {
    for (int i = 0; i < _tasks.length; i++) {
      debugPrint(
          'Task ${i + 1} name ${_tasks[i].name} description: ${_tasks[i].description}');
    }
  }

  void clearTasks() {
    _tasks.clear();
    storageList.clear();
    box.erase();
  }

  @override
  void initState() {
    super.initState();
    restoreTasks(); // restore list from storing in initState
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: Container(),
          ),
          TextButton(
            onPressed: () {
              final task =
                  Task(description: 'test description', name: 'test name');
              addAndStoreTask(task);
            },
            child: Text('Add Task'),
          ),
          TextButton(
            onPressed: () {
              printTasks();
            },
            child: Text('Print Storage'),
          ),
          TextButton(
            onPressed: () {
              clearTasks();
            },
            child: Text('Clear Tasks'),
          ),
        ],
      ),
    );
  }
}

0
欢迎来到Flutter和Dart!一开始可能会感到有些困难,但后来会很有收获。从逻辑上考虑,数据应该如何存储在应用程序状态之外呢?它必须从某个数据存储中获取,可以是设备存储,也可以是远程数据库。
对于设备存储,您有两个选择,但对于这个任务来说,SQlite可能过于复杂了,然而sharedPreferences并不像看起来那么困难,一旦掌握了使用方法就非常容易。
基本上,它是将数据以字符串的形式存储在设备上,并附带一个唯一的键来稍后检索该数据。这个键也是一个字符串。
对于您的应用程序,您只需要两个键:“tasks”和“completedTasks”。
您遇到了什么问题?将数据编码为字符串?还是先将其转换为映射再进行编码?
您的另一个选择是远程数据库,通过互联网发送数据,Firebase只是可能的解决方案之一,除了构建自己的服务器、API和数据库之外,这对于这种情况来说肯定也是过于复杂了。

然而,由于您仍在摸索中,这将是一个很好的项目,可以从共享首选项开始,然后在第二阶段研究Firebase,以便您的待办事项可以在多个设备上同步。这也带来了学习Firebase的额外好处,我强烈建议您深入了解。

每一门新语言在开始时都是令人生畏的,但期望从一开始就像1+1=2一样简单并不能让您达到目标,我相信您学习Flutter不仅仅是为了制作待办事项应用程序,而是从这个应用程序中学到的知识将为未来做好准备。

我的建议是,要适应不适应,记住您开始这段旅程的原因,无论需要什么帮助,社区永远不会让您失望,只要与我们或自己半途而废。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接