Flutter Development-5: Adding Local Storage

Welcome to the fifth post in our series on mobile app development using Flutter. In this post, we’ll implement local storage functionality in our To-Do list app to achieve data persistence.

Why Do We Need Local Storage?

Data persistence is crucial in mobile apps. Maintaining user data even after the app is closed and reopened provides a better user experience. This is especially important for apps like our To-Do list.

Introduction to SharedPreferences

Flutter provides a plugin called ‘SharedPreferences’ for storing simple key-value pairs of data. This plugin offers an easy way to use iOS’s NSUserDefaults and Android’s SharedPreferences in Flutter.

1. Adding the Package

First, we need to add the shared_preferences package. Add the following line to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.0.8

Then, run this command in your terminal:

flutter pub get

2. Modifying the Code

Now, let’s modify our lib/main.dart file as follows:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'To-Do List',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: TodoListScreen(),
    );
  }
}

class TodoListScreen extends StatefulWidget {
  @override
  _TodoListScreenState createState() => _TodoListScreenState();
}

class _TodoListScreenState extends State<TodoListScreen> {
  List<Todo> todos = [];

  @override
  void initState() {
    super.initState();
    _loadTodos();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My To-Do List'),
      ),
      body: TodoList(todos: todos, onTodoToggle: _toggleTodo),
      floatingActionButton: FloatingActionButton(
        onPressed: _addTodo,
        child: Icon(Icons.add),
      ),
    );
  }

  void _addTodo() {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        String newTodo = "";
        return AlertDialog(
          title: Text('Add a new todo'),
          content: TextField(
            onChanged: (value) {
              newTodo = value;
            },
          ),
          actions: <Widget>[
            TextButton(
              child: Text('Add'),
              onPressed: () {
                setState(() {
                  todos.add(Todo(title: newTodo));
                  _saveTodos();
                });
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

  void _toggleTodo(Todo todo) {
    setState(() {
      todo.isCompleted = !todo.isCompleted;
      _saveTodos();
    });
  }

  void _loadTodos() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String? todosString = prefs.getString('todos');
    if (todosString != null) {
      List<dynamic> todoList = jsonDecode(todosString);
      setState(() {
        todos = todoList.map((item) => Todo.fromJson(item)).toList();
      });
    }
  }

  void _saveTodos() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String todosString = jsonEncode(todos.map((todo) => todo.toJson()).toList());
    await prefs.setString('todos', todosString);
  }
}

class TodoList extends StatelessWidget {
  final List<Todo> todos;
  final Function(Todo) onTodoToggle;

  TodoList({required this.todos, required this.onTodoToggle});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(
            todos[index].title,
            style: TextStyle(
              decoration: todos[index].isCompleted ? TextDecoration.lineThrough : null,
            ),
          ),
          trailing: Checkbox(
            value: todos[index].isCompleted,
            onChanged: (_) => onTodoToggle(todos[index]),
          ),
        );
      },
    );
  }
}

class Todo {
  String title;
  bool isCompleted;

  Todo({required this.title, this.isCompleted = false});

  factory Todo.fromJson(Map<String, dynamic> json) {
    return Todo(
      title: json['title'],
      isCompleted: json['isCompleted'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'title': title,
      'isCompleted': isCompleted,
    };
  }
}

3. Code Explanation

  • _loadTodos() method: Loads saved To-Do items from SharedPreferences when the app starts. This method is called in initState() to load data at app startup.
  • _saveTodos() method: Saves To-Do list to SharedPreferences whenever it changes. This method is called when adding new items or changing completion status.
  • Todo class: Added fromJson and toJson methods to enable JSON conversion. This is necessary for storing and retrieving objects.

The Need for JSON Serialization

SharedPreferences can only store basic data types like strings, integers, and booleans. However, our To-Do items are objects. To solve this, we convert To-Do objects to JSON format for storage and back to objects when retrieving. This process is called JSON serialization and deserialization.

The Importance of Asynchronous Programming

Storing and retrieving data from local storage takes time. Processing these operations synchronously on the main thread can degrade app performance and user experience. Therefore, we handle these operations asynchronously. Dart provides ‘async’ and ‘await’ keywords to easily implement asynchronous programming.

Running the App

Start the app by running this command in your terminal:

flutter pub run

Now when you run the app, To-Do items will be saved locally, persisting even when you close and reopen the app.

Wrap-up

In this post, we added local storage functionality to our To-Do list app, implementing data persistence. Through this, we learned about data storage methods in Flutter and the basics of asynchronous programming. By implementing local storage in this way, our To-Do list app now ensures user data persistence, providing a better user experience.

In our next post, we’ll improve the UI of this app and implement additional features such as deleting and editing tasks. If you have any questions about Flutter development, feel free to leave a comment below!

Related Resources

Related Posts

Flutter Development-3: Implementing a Simple To-Do List App UI – CSAI

Flutter Development-2 Project Creation and Structure Overview – CSAI

Flutter Development-3: Implementing a Simple To-Do List App UI – CSAI

Flutter-4: Adding State Management – CSAI

Leave a Reply

Your email address will not be published. Required fields are marked *