Menu
CoddyTech

Danh sách việc cần làm: Vuốt để xóa

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

Trên điện thoại, bạn xóa một công việc đã hoàn thành bằng cách vuốt nó đi. Bọc mỗi hàng trong một Dismissible, Flutter sẽ tự xử lý hiệu ứng trượt cho bạn:

Dismissible(
  key: ObjectKey(task),
  direction: DismissDirection.endToStart,
  background: Container(color: Colors.red, ...),
  onDismissed: (direction) => _deleteTask(task),
  child: ListTile(...),
)

key cho Flutter biết hàng nào là hàng nào, nhờ đó hàng cần xóa sẽ trượt ra. ObjectKey(task) gắn hàng với chính công việc đó. endToStart nghĩa là vuốt sang trái, còn background là phần hiển thị phía sau hàng khi nó trượt: màu đỏ với biểu tượng xóa ở bên phải.

onDismissed chạy khi hàng biến mất khỏi màn hình. Trong đó, bạn cần xóa công việc khỏi danh sách bằng setState. Hãy xóa chính công việc đó, không phải vị trí của nó, để xóa đúng công việc ngay cả khi danh sách thay đổi sau này.

challenge icon

Thử thách

Trung bình

Vuốt để xóa công việc:

  • thêm phương thức _deleteTask(Task task) để xóa công việc bên trong setState
  • bọc mỗi ListTile trong một Dismissible với key: ObjectKey(task), có thể vuốt sang trái
  • background của mục này là một Container màu đỏ, có biểu tượng Icons.delete màu trắng ở bên phải, cách mép 24 pixel

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();
  }

  @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 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