Persiting items using shared_preferences
linked to #18
At the moment we can create new task and toggle then from uncomplete to complete. However the state of the application is not saved and each time the application is restarted the items created are lost.
Using the shared preferences package we can save a serialized version of the application state (using json).
- [x] The first step is to know how to initialise the state of statefull widgets with initState
- [x] Once the list of items is loaded from the disk we will need to save the state each time an item is created and each time an item is toggled.
see: https://pub.dev/packages/shared_preferences
the initState method is defined in the StatefullWidget abstract class.
It is called the first time the widget is added to the widget tree.
We can override this method in our statefull widget. In our case we want to load all the tasks that are saved in the disk.
To test this method we can first add manually new task to the list of tasks and see if the application display them properly:
@override
void initState() {
_tasks
..add(Task(text: 'default task'))
..add(Task(text: 'anotherstuff', completed: true));
super.initState();
}

Because the initState function belongs to the statefull widget class it can directly access the instance variable _tasks.
We then use the cascade operator .. (see https://github.com/dwyl/learn-dart/issues/7) to add two tasks to the list (the last one is marked as completed)
The initState method is linked to the mustCallSuper annotation. ~That's why the last line is super.initState()~ super.initState() needs to be called at the top of the function.
See the following commit: https://github.com/dwyl/flutter-todo-list-tutorial/pull/17/commits/920117d2e9b28747054066a6a47980347ce2e1fa
To save the tasks list on disk we need to serialise it first. Our models are quiet small at the moment so we can't manually create the fromJson contructor and toJson method to encode/decode the task values. See https://flutter.dev/docs/development/data-and-backend/json. The Task class looks like this:
class Task {
final String text;
bool completed;
Task({this.text, this.completed = false});
// use initializer list to set the text and completed value of the fromJson constructor
Task.fromJson(Map<String, dynamic> json)
: text = json['text'],
completed = json['copmpleted'];
Map<String, dynamic> toJson() => {'text': text, 'completed': completed};
}
At the moment I have the following code which get the tasks from the shared preferences via an async function and I call this function in the initSate method:
Future<List<Task>> getTasksFromSharedPrefs() async {
final prefs = await SharedPreferences.getInstance();
final tasks = prefs.getString('tasks') ?? '[]';
final List<Map<String, dynamic>> jsonListTasks = jsonDecode(tasks);
return jsonListTasks.map((Map<String, dynamic> m) => Task.fromJson(m));
}
@override
Future<void> initState() async {
// super.initState must be called first
super.initState();
_tasks = await getTasksFromSharedPrefs();
}
However this returns an error which tells us that initState must returns a void and that we can't use a Future as a returned type. It seems possible to still call the getTasksFromSharedPrefs() method in initState using anonymous function but I don't think having async function called there is a good idea.
Looking at instead at using a FutureBuilder to create the list of tasks from the async operation of fetching the data from the disk.
If the async function returns Future<void> it is then possible to use it in initState.
The async function takes care then of assigning the value using setState:
Future<void> getTasksFromSharedPrefs() async {
final prefs = await SharedPreferences.getInstance();
final tasksJson = prefs.getString('tasks') ?? '[]';
// https://flutter.dev/docs/cookbook/networking/background-parsing#convert-the-response-into-a-list-of-photos
final jsonListTasks = jsonDecode(tasksJson).cast<Map<String, dynamic>>();
final tasks = jsonListTasks.map<Task>((m) => Task.fromJson(m)).toList();
setState(() {
_tasks = tasks;
});
}
@override
void initState() {
// super.initState must be called first
super.initState();
getTasksFromSharedPrefs();
}
}
This works, however the FutureBuilder might still be more adequate in this situation.
With the latest commit: https://github.com/dwyl/flutter-todo-list-tutorial/pull/17/commits/9cb1ee21dd2200bf238ae2670d02a17de62b5791 I'm now able to save new created task using sharedpreferences and also able to retrieve the list of tasks created when the application is opened.
I'm using jsonEncode and jsonDecode to save task objects as string on the disk. These two functions are using the toJson task method and the fromJson constructor task, so you need to define them in the Task class.
There is however an issue when a task is toggle between completed/uncompleted. The task widgets don't have access directly to the list of tasks state. So when a toggle event is triggered we don't save the state of the list of tasks. I've tried to access the list state via the task widgets but didn't find a nice manual solution. However this is where a state management tool can be introduce to make it easier for the application to get notify when the state of the list of tasks change and that's why I'm now looking at using provider. See also https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
While it's good to understand how shared_preferences work, do you feel that using them for storing Todo item data is appropriate? Should we just skip this step and go straight to using drift? https://github.com/dwyl/learn-flutter/issues/70 💭