Danh sách việc cần làm: Trạng thái trống
Thuộc phần Dự án trong hành trình Flutter của Coddy. Bài học 9 trên 36.
Nếu bạn chạm vào Done trước khi hoàn thành việc nào, bạn sẽ thấy màn hình trống. Ứng dụng bị lỗi, hay không có gì ở đó? Trạng thái trống sẽ giải đáp điều đó bằng một biểu tượng nhẹ nhàng và một dòng mô tả tình trạng.
Mỗi bộ lọc có thông báo riêng, vì vậy biểu thức switch trên _filter sẽ chọn thông báo tương ứng:
String get _emptyMessage => switch (_filter) {
Filter.all => 'No tasks yet',
Filter.active => 'Nothing left to do',
Filter.done => 'Nothing done yet',
};Tiếp đó, danh sách sẽ hiển thị phần này hoặc phần kia: shown.isEmpty ? Center(...) : ListView.builder(...). Hãy kiểm tra danh sách đang hiển thị, không phải _tasks: khi chọn Done, danh sách đầy đủ không trống nhưng màn hình lại trống.
Dùng màu outline của giao diện cho biểu tượng: Theme.of(context).colorScheme.outline. Đây là sắc xám dịu của màu chủ đạo, giúp trạng thái trống trông như một phần của ứng dụng.
Đây là bài học cuối cùng về danh sách việc cần làm. Các bước kiểm tra sẽ thử toàn bộ ứng dụng: thêm, đánh dấu, vuốt và lọc.
Thử thách
Trung bìnhHiển thị trạng thái trống khi shown trống:
- thêm getter
_emptyMessage:No tasks yetcho Tất cả,Nothing left to docho Đang thực hiện,Nothing done yetcho Đã xong - thay cho danh sách, hiển thị một
Columncăn giữa gồm biểu tượngIcons.task_alt(kích thước 64, màuoutlinecủa giao diện), khoảng cách 12 pixel và thông báo - nếu không, hiển thị danh sách như trước
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;
}
enum Filter { all, active, done }
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();
Filter _filter = Filter.all;
int get _left => _tasks.where((task) => !task.done).length;
List<Task> get _shown => switch (_filter) {
Filter.all => _tasks,
Filter.active => _tasks.where((task) => !task.done).toList(),
Filter.done => _tasks.where((task) => task.done).toList(),
};
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) {
final index = _tasks.indexOf(task);
setState(() {
_tasks.remove(task);
});
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Deleted ${task.title}'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
setState(() {
_tasks.insert(index, task);
});
},
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final shown = _shown;
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,
),
],
),
),
SegmentedButton<Filter>(
segments: const [
ButtonSegment(value: Filter.all, label: Text('All')),
ButtonSegment(value: Filter.active, label: Text('Active')),
ButtonSegment(value: Filter.done, label: Text('Done')),
],
selected: {_filter},
onSelectionChanged: (selection) {
setState(() {
_filter = selection.first;
});
},
),
const SizedBox(height: 8),
Expanded(
child: ListView.builder(
itemCount: shown.length,
itemBuilder: (context, index) {
final task = shown[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,
),
),
);
},
),
),
],
),
);
}
}
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
1Dự án: Danh sách việc cần làm
Danh sách việc cần làm: Danh sách công việcDanh sách việc cần làm: Thêm công việc4Dự án: Theo dõi thói quen
Theo dõi thói quen: Thói quen của bạnTheo dõi thói quen: TuầnTự luyện tập: Trình biên dịch Flutter online