Menu
CoddyTech

Danh sách việc cần làm: Hoàn tác xóa

Thuộc phần Dự án trong hành trình Flutter của Coddy. Bài học 7 trên 36.

Một thao tác vuốt diễn ra nhanh, và vuốt nhầm cũng vậy. Ứng dụng tốt cho phép bạn hoàn tác: một thông báo ở cuối màn hình cho biết mục nào đã bị xóa, kèm nút Hoàn tác bên cạnh.

SnackBar nhận action:, một SnackBarAction có label và onPressed. Để đưa tác vụ về vị trí cũ, hãy ghi nhớ vị trí của nó trước khi xóa:

final index = _tasks.indexOf(task);
// Xóa tác vụ đó, rồi hiển thị SnackBar
SnackBarAction(
  label: 'Undo',
  onPressed: () {
    setState(() {
      _tasks.insert(index, task);
    });
  },
)

Vuốt nhanh hai tác vụ thì thông báo thứ hai sẽ chờ đến khi thông báo đầu tiên hết thời gian hiển thị. Gọi ScaffoldMessenger.of(context).hideCurrentSnackBar(); trước showSnackBar để thông báo mới nhất hiện ngay lập tức.

insert(index, task) đưa tác vụ về vị trí đó và đẩy các tác vụ phía sau xuống. add sẽ đưa tác vụ vào cuối danh sách.

Hàm bạn truyền cho onPressed ghi nhớ index và task tại thời điểm bạn tạo hàm, dù hàm chạy sau đó. Đó là một closure, và callback luôn dựa vào nó.

challenge icon

Thử thách

Trung bình

Cho phép hoàn tác thao tác xóa. Trong _deleteTask:

  • trước khi xóa công việc, lưu lại vị trí của nó bằng indexOf
  • sau khi xóa, hiển thị SnackBar với nội dung Deleted và tiêu đề công việc, chẳng hạn như Deleted Walk the dog
  • thêm thao tác Undo để đưa công việc trở lại vị trí cũ
  • ẩn thông báo đang hiển thị trước, để thao tác xóa mới thay thế thông báo đó

Tự làm thử

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
      ),
      home: const TodoScreen(),
    );
  }
}

class Task {
  Task(this.title);

  final String title;
  bool done = false;
}

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State<TodoScreen> createState() => _TodoScreenState();
}

class _TodoScreenState extends State<TodoScreen> {
  final List<Task> _tasks = [Task('Buy milk'), Task('Walk the dog'), Task('Read a book')];
  final _controller = TextEditingController();

  int get _left => _tasks.where((task) => !task.done).length;

  void _addTask() {
    final title = _controller.text.trim();
    if (title.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Type a task first')),
      );
      return;
    }
    setState(() {
      _tasks.add(Task(title));
    });
    _controller.clear();
  }

  void _deleteTask(Task task) {
    setState(() {
      _tasks.remove(task);
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('My Tasks')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      labelText: 'New task',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                IconButton.filled(
                  onPressed: _addTask,
                  icon: const Icon(Icons.add),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: Row(
              children: [
                Text(
                  _left == 1 ? '1 task left' : '$_left tasks left',
                  style: Theme.of(context).textTheme.titleSmall,
                ),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _tasks.length,
              itemBuilder: (context, index) {
                final task = _tasks[index];
                return Dismissible(
                  key: ObjectKey(task),
                  direction: DismissDirection.endToStart,
                  background: Container(
                    color: Colors.red,
                    alignment: Alignment.centerRight,
                    padding: const EdgeInsets.symmetric(horizontal: 24),
                    child: const Icon(Icons.delete, color: Colors.white),
                  ),
                  onDismissed: (direction) => _deleteTask(task),
                  child: ListTile(
                    leading: Checkbox(
                      value: task.done,
                      onChanged: (value) {
                        setState(() {
                          task.done = value!;
                        });
                      },
                    ),
                    title: Text(
                      task.title,
                      style: task.done
                          ? const TextStyle(
                              decoration: TextDecoration.lineThrough,
                              color: Colors.grey,
                            )
                          : null,
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Dự án

Tự luyện tập: Trình biên dịch Flutter online