Browse State Management Essentials

Efficiently Managing Todos: Adding, Updating, and Deleting with Provider

Learn how to manage todo items in a Flutter app using the Provider package. This guide covers adding, updating, and deleting todos, along with handling edge cases and testing functionality.

3.3.4 Adding, Updating, and Deleting Todos

Managing a list of todos is a classic example of CRUD (Create, Read, Update, Delete) operations in application development. In this section, we’ll explore how to implement these operations in a Flutter app using the Provider package. We’ll cover how to add new todos, update existing ones, delete them, and toggle their completion status. Additionally, we’ll discuss handling edge cases and testing the functionality to ensure a robust application.

Adding Todos

Adding a new todo involves collecting data from the user, creating a Todo object, and adding it to the list managed by the TodoProvider. Here’s how you can achieve this:

Collecting User Input

To add a new todo, you typically need a user interface where users can enter the todo details. This could be a simple form with a TextField for the todo title and an optional description.

class AddTodoScreen extends StatelessWidget {
  final TextEditingController _titleController = TextEditingController();
  final TextEditingController _descriptionController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Add Todo')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _titleController,
              decoration: InputDecoration(labelText: 'Title'),
            ),
            TextField(
              controller: _descriptionController,
              decoration: InputDecoration(labelText: 'Description'),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                final newTodo = Todo(
                  id: DateTime.now().toString(),
                  title: _titleController.text,
                  description: _descriptionController.text,
                  isCompleted: false,
                );
                Provider.of<TodoProvider>(context, listen: false).addTodo(newTodo);
                Navigator.pop(context);
              },
              child: Text('Add Todo'),
            ),
          ],
        ),
      ),
    );
  }
}

Adding the Todo to the Provider

The TodoProvider class should have a method to add a new todo to the list and notify listeners about the change.

class TodoProvider with ChangeNotifier {
  List<Todo> _todos = [];

  List<Todo> get todos => _todos;

  void addTodo(Todo todo) {
    _todos.add(todo);
    notifyListeners();
  }
}

Updating Todos

Updating a todo involves passing the existing Todo object to an edit screen, allowing the user to modify it, and then saving the changes back to the provider.

Passing the Todo to the Edit Screen

When navigating to the edit screen, pass the todo object that needs to be edited.

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => EditTodoScreen(todo: todo),
  ),
);

Implementing the Update Method

The TodoProvider should have a method to update an existing todo. This involves finding the todo by its ID, updating its details, and notifying listeners.

void updateTodo(Todo todo) {
  int index = _todos.indexWhere((t) => t.id == todo.id);
  if (index != -1) {
    _todos[index] = todo;
    notifyListeners();
  }
}

Code Example for the Edit Screen

class EditTodoScreen extends StatelessWidget {
  final Todo todo;
  final TextEditingController _titleController;
  final TextEditingController _descriptionController;

  EditTodoScreen({required this.todo})
      : _titleController = TextEditingController(text: todo.title),
        _descriptionController = TextEditingController(text: todo.description);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Edit Todo')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _titleController,
              decoration: InputDecoration(labelText: 'Title'),
            ),
            TextField(
              controller: _descriptionController,
              decoration: InputDecoration(labelText: 'Description'),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                final updatedTodo = Todo(
                  id: todo.id,
                  title: _titleController.text,
                  description: _descriptionController.text,
                  isCompleted: todo.isCompleted,
                );
                Provider.of<TodoProvider>(context, listen: false).updateTodo(updatedTodo);
                Navigator.pop(context);
              },
              child: Text('Update Todo'),
            ),
          ],
        ),
      ),
    );
  }
}

Deleting Todos

Deleting a todo can be achieved by providing a delete button or a swipe action in the UI. The TodoProvider should have a method to remove a todo by its ID.

Implementing the Delete Method

void deleteTodo(String id) {
  _todos.removeWhere((todo) => todo.id == id);
  notifyListeners();
}

UI for Deleting Todos

You can add a delete button in the todo list item or implement a swipe-to-delete action.

ListView.builder(
  itemCount: todos.length,
  itemBuilder: (context, index) {
    final todo = todos[index];
    return Dismissible(
      key: Key(todo.id),
      onDismissed: (direction) {
        Provider.of<TodoProvider>(context, listen: false).deleteTodo(todo.id);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('${todo.title} deleted')),
        );
      },
      background: Container(color: Colors.red),
      child: ListTile(
        title: Text(todo.title),
        subtitle: Text(todo.description),
        trailing: IconButton(
          icon: Icon(Icons.delete),
          onPressed: () {
            Provider.of<TodoProvider>(context, listen: false).deleteTodo(todo.id);
          },
        ),
      ),
    );
  },
)

Toggling Completion Status

Allow users to mark todos as complete or incomplete by updating the isCompleted property and notifying listeners.

Toggling Method in Provider

void toggleTodoCompletion(String id) {
  int index = _todos.indexWhere((todo) => todo.id == id);
  if (index != -1) {
    _todos[index].isCompleted = !_todos[index].isCompleted;
    notifyListeners();
  }
}

UI for Toggling Completion

You can use a checkbox or a toggle button in the todo list item.

ListTile(
  title: Text(todo.title),
  subtitle: Text(todo.description),
  trailing: Checkbox(
    value: todo.isCompleted,
    onChanged: (value) {
      Provider.of<TodoProvider>(context, listen: false).toggleTodoCompletion(todo.id);
    },
  ),
)

Handling Edge Cases

Handling edge cases is crucial for providing a smooth user experience and preventing errors.

Error Handling

  • Updating Non-Existent Todos: Ensure the update method checks if the todo exists before updating.
  • Deleting Non-Existent Todos: Similarly, ensure the delete method checks for existence.
  • User Confirmation: Consider asking for user confirmation before deleting a todo to prevent accidental deletions.

User Experience Considerations

  • Feedback Messages: Provide feedback to users after actions, such as showing a snackbar message after a todo is deleted.
  • Undo Options: Implement an undo feature for delete actions to enhance user experience.

Testing Functionality

Thorough testing of all CRUD operations ensures the reliability of your application.

Unit Testing the TodoProvider

Write unit tests to verify that the TodoProvider methods work as expected.

void main() {
  group('TodoProvider', () {
    test('adds a todo', () {
      final provider = TodoProvider();
      final todo = Todo(
        id: '1',
        title: 'Test Todo',
        description: 'Test Description',
        isCompleted: false,
      );

      provider.addTodo(todo);

      expect(provider.todos.contains(todo), true);
    });

    test('updates a todo', () {
      final provider = TodoProvider();
      final todo = Todo(
        id: '1',
        title: 'Test Todo',
        description: 'Test Description',
        isCompleted: false,
      );
      provider.addTodo(todo);

      final updatedTodo = Todo(
        id: '1',
        title: 'Updated Todo',
        description: 'Updated Description',
        isCompleted: true,
      );
      provider.updateTodo(updatedTodo);

      expect(provider.todos.first.title, 'Updated Todo');
      expect(provider.todos.first.isCompleted, true);
    });

    test('deletes a todo', () {
      final provider = TodoProvider();
      final todo = Todo(
        id: '1',
        title: 'Test Todo',
        description: 'Test Description',
        isCompleted: false,
      );
      provider.addTodo(todo);

      provider.deleteTodo(todo.id);

      expect(provider.todos.contains(todo), false);
    });
  });
}

Conclusion

Managing todos in a Flutter app using the Provider package involves implementing CRUD operations effectively. By understanding how to add, update, delete, and toggle todos, you can create a responsive and user-friendly application. Handling edge cases and testing functionality are crucial steps in ensuring the robustness of your app. As you implement these features, consider the user experience and strive for a seamless interaction with your application.

Quiz Time!

### What method is used to notify listeners about changes in the `TodoProvider`? - [x] notifyListeners() - [ ] updateListeners() - [ ] refreshListeners() - [ ] changeListeners() > **Explanation:** The `notifyListeners()` method is used in the `ChangeNotifier` class to notify all listeners about changes, prompting them to rebuild their widgets. ### How do you add a new todo in the `TodoProvider`? - [x] By calling `addTodo()` method with a `Todo` object. - [ ] By directly modifying the `_todos` list. - [ ] By using the `updateTodo()` method. - [ ] By calling `notifyListeners()` without adding the todo. > **Explanation:** The `addTodo()` method is specifically designed to add a new `Todo` object to the list and notify listeners of the change. ### What is the purpose of the `updateTodo()` method in the `TodoProvider`? - [x] To update an existing todo's details. - [ ] To add a new todo to the list. - [ ] To delete a todo from the list. - [ ] To toggle the completion status of a todo. > **Explanation:** The `updateTodo()` method updates the details of an existing todo and notifies listeners of the change. ### How can you delete a todo from the list? - [x] By calling the `deleteTodo()` method with the todo's ID. - [ ] By setting the todo's `isCompleted` property to `true`. - [ ] By using the `updateTodo()` method. - [ ] By directly removing the todo from the `_todos` list without notifying listeners. > **Explanation:** The `deleteTodo()` method removes a todo from the list by its ID and notifies listeners of the change. ### What UI element can be used to toggle a todo's completion status? - [x] Checkbox - [ ] TextField - [ ] ElevatedButton - [ ] ListTile > **Explanation:** A `Checkbox` is commonly used to toggle a todo's completion status, allowing users to mark it as complete or incomplete. ### Why is it important to handle edge cases in CRUD operations? - [x] To ensure a smooth user experience and prevent errors. - [ ] To increase the complexity of the code. - [ ] To make the application slower. - [ ] To reduce the number of features. > **Explanation:** Handling edge cases is crucial for providing a smooth user experience and preventing errors that could disrupt the application's functionality. ### What is a recommended practice before deleting a todo? - [x] Asking for user confirmation. - [ ] Automatically deleting without notice. - [ ] Updating the todo instead. - [ ] Adding the todo again. > **Explanation:** Asking for user confirmation before deleting a todo helps prevent accidental deletions and improves user experience. ### What is the benefit of writing unit tests for the `TodoProvider`? - [x] To verify that CRUD operations work as expected. - [ ] To make the code more complex. - [ ] To slow down the development process. - [ ] To reduce the number of features. > **Explanation:** Writing unit tests helps ensure that CRUD operations in the `TodoProvider` work correctly, improving the reliability of the application. ### Which method is used to pass a `Todo` object to the edit screen? - [x] Navigator.push() - [ ] Navigator.pop() - [ ] Navigator.replace() - [ ] Navigator.remove() > **Explanation:** `Navigator.push()` is used to navigate to a new screen, passing the `Todo` object as an argument. ### True or False: The `TodoProvider` should directly expose the `_todos` list for external modification. - [ ] True - [x] False > **Explanation:** The `TodoProvider` should not expose the `_todos` list directly for modification. Instead, it should provide methods to modify the list and notify listeners of changes.