Flutter - Best way to store a List of Objects every time I close the application?

5,156

2022 Update with null safety

My original code example was more verbose than necessary. Using Darts factory constructor this can be done with way less code. This is also updated for null safety and using Hive instead of GetStorage.

First, add a toMap method which converts the object to a Map, then a fromMap constructor which returns a Task object from a Map that was saved in storage.

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';
  }
}

Updated Demo Page


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'),
          ),
        ],
      ),
    );
  }
}

Updated Storage Init

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

Original Answer

So generally speaking, once you want to store anything other than a primitive type ie. String int etc... things get a bit more complex because they have to converted to something that's readable by any storage solution.

So despite Tasks being a basic object with a couple strings, SharedPreferences or anything else doesn't know what a Task is or what to do with it.

I suggest in general reading about json serialization, as you'll need to know about it either way. This is a good place to start and here is another good article about it.

All that being said, it can also be done without json by converting your task to a Map (which is what json serialization does anyway) and storing it to a list of maps. I'll show you an example of doing this manually without json. But again, its in your best interest to buckle down and spend some time learning it.

This example will use Get Storage, which is like SharedPreferences but easier because you don't need separate methods for different data types, just read and write.

I don't know how you're adding tasks in your app, but this is just a basic example of storing a list of Task objects. Any solution that doesn't involve online storage requires storing locally, and retrieving from storage on app start.

So let's say here is your Task object.

class Task {
  final String name;
  final String description;

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

Put this in your main method before running your app

await GetStorage.init();

You'll need to add async to your main, so if you're not familiar with how that works it looks like this.

void main() async {
  await GetStorage.init();

  runApp(MyApp());
}

Normally I would NEVER do all this logic inside a stateful widget, but instead implement a state management solution and do it in a class outside of the UI, but that's a whole different discussion. I also recommend checking out GetX, Riverpod, or Provider reading about them and seeing which one strikes you as the easiest to learn. GetX gets my vote for simplicity and functionality.

But since you're just starting out I'll omit that part of it and just put all these functions in the UI page for now.

Also instead of only storing when app closes, which can also be done, its easier to just store anytime there is a change to the list.

Here's a page with some buttons to add, clear, and print storage so you can see exactly whats in your list after app restart.

If you understand whats going on here you should be able to do this in your app, or study up on json and do it that way. Either way, you need to wrap your head around Maps and how local storage works with any of the available solutions.

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'),
          ),
        ],
      ),
    );
  }
}
Share:
5,156
Louis
Author by

Louis

Updated on December 20, 2022

Comments

  • Louis
    Louis over 1 year

    The situation:
    I'm very new to Flutter and mobile development, thus I don't know much about Dart; And I've read some solutions from people with similar problems but didn't manage to work these solutions to my own thing.

    The problem:
    I have a to-do app that has 2 Lists of Objects, and I want to store those Lists for whenever the user re-open the app.

    I know its simple stuff but I feel like I'm storming towards this problem due to the lack of experience... And so I decided to come asking for some light.

    What I've tried:
    I have come across different solutions for this problem and all of them seemed way too complex to this case (compared to what I'm used to do when saving lists to the archive), including: encoding the list to a map and converting to a string before using SharedPreferences, using SQlite database (every tutorial I've come across made me feel like I'd be using a war tank to kill an ant, I'd say the same about firebase).

    Structure of the problem:
    ToDo screen with a ListView.builder calling 2 arrays: ongoing tasks and done tasks each of which I want to write to the phone whenever the user makes a change. IDK if I should only try to save those arrays from within the class from which they belong by calling some packages methods, or if I should try to store the entire application if such thing is possible.

    Conclusion:
    Is there a way to solve this in a simple way or I should use something robust like firebase just for that? even though I'm not used to work with firestore, and so I'm in the dark not knowing how to apply such thing to save data.

    How my lists are structured:

    List<Task> _tasks = [
        Task(
          name: "do something",
          description: "try to do it fast!!!",
        ),
      ];
    
    List<Task> _doneTasks = [
     Task(
          name: "task marked as done",
          description: "something",
        ),
    ];
    
    • Louis
      Louis about 3 years
      @Loren.A Thanks for the help! i managed to work with provider alongside the get_storage logic u presented.. I wonder how would i remove an element from that storagelist. I tried doing storageList.remove('tasks') but nothing happaned; when i tried to call the object box.remove('tasks') the app crashed - NoSuchMethodError: the getter 'length' was called on null --- that ocurred during restoreTasks's loop between storageList elements.