✅ Todo List
Xây dựng ứng dụng Todo List đầy đủ chức năng. ⏱️ 30 phút
🎯 Mục tiêu
Phần tiêu đề “🎯 Mục tiêu”Tạo ứng dụng Todo với các tính năng:
- Thêm task mới
- Đánh dấu hoàn thành/chưa hoàn thành
- Xóa task
- Đếm số task còn lại
- Lọc theo trạng thái
🚀 Bắt đầu
Phần tiêu đề “🚀 Bắt đầu”Bước 1: Cấu trúc cơ bản
Phần tiêu đề “Bước 1: Cấu trúc cơ bản”Tạo file todo_app.py:
import flet as ft
@ft.observableclass TodoState: items: list = []
def add(self, text: str): if text: self.items.append({ "id": len(self.items), "text": text, "done": False })
state = TodoState()
def main(page: ft.Page): page.title = "Todo App" page.add(ft.Text("Todo App", size=24))
ft.run(main)Bước 2: Form thêm task
Phần tiêu đề “Bước 2: Form thêm task”import flet as ft
@ft.observableclass TodoState: items: list = []
def add(self, text: str): if text.strip(): self.items.append({ "id": len(self.items), "text": text.strip(), "done": False })
state = TodoState()
@ft.componentdef AddTodoForm(): input_value, set_input = ft.use_state("")
def add_todo(_): state.add(input_value) set_input("")
return ft.Row([ ft.TextField( value=input_value, hint_text="Nhập việc cần làm...", expand=True, on_change=lambda e: set_input(e.control.value), on_submit=add_todo, ), ft.FilledButton("Thêm", icon=ft.Icons.ADD, on_click=add_todo), ])
def main(page: ft.Page): page.title = "Todo App" page.padding = 20 page.add( ft.Text("📝 Todo List", size=28, weight=ft.FontWeight.BOLD), AddTodoForm(), )
ft.run(main)Bước 3: Hiển thị danh sách tasks
Phần tiêu đề “Bước 3: Hiển thị danh sách tasks”import flet as ft
@ft.observableclass TodoState: items: list = []
def add(self, text: str): if text.strip(): self.items.append({ "id": len(self.items), "text": text.strip(), "done": False })
def toggle(self, id: int): for item in self.items: if item["id"] == id: item["done"] = not item["done"] break
def delete(self, id: int): self.items = [item for item in self.items if item["id"] != id]
state = TodoState()
@ft.componentdef AddTodoForm(): input_value, set_input = ft.use_state("")
def add_todo(_): state.add(input_value) set_input("")
return ft.Row([ ft.TextField( value=input_value, hint_text="Nhập việc cần làm...", expand=True, on_change=lambda e: set_input(e.control.value), on_submit=add_todo, ), ft.FilledButton("Thêm", icon=ft.Icons.ADD, on_click=add_todo), ])
@ft.componentdef TodoItem(item): return ft.ListTile( leading=ft.Checkbox( value=item["done"], on_change=lambda _: state.toggle(item["id"]), ), title=ft.Text( item["text"], style=ft.TextStyle( decoration=ft.TextDecoration.LINE_THROUGH if item["done"] else None, color=ft.Colors.GREY_500 if item["done"] else None, ), ), trailing=ft.IconButton( ft.Icons.DELETE, icon_color=ft.Colors.RED_300, on_click=lambda _: state.delete(item["id"]), ), )
@ft.componentdef TodoList(): if not state.items: return ft.Container( content=ft.Text("✨ Chưa có việc nào. Thêm task đi!", color=ft.Colors.GREY_500), padding=40, alignment=ft.alignment.center, )
return ft.Column([ TodoItem(item) for item in state.items ])
def main(page: ft.Page): page.title = "Todo App" page.padding = 20 page.add( ft.Text("📝 Todo List", size=28, weight=ft.FontWeight.BOLD), AddTodoForm(), ft.Divider(), TodoList(), )
ft.run(main)Bước 4: Thêm bộ lọc và đếm
Phần tiêu đề “Bước 4: Thêm bộ lọc và đếm”import flet as ft
@ft.observableclass TodoState: items: list = [] filter: str = "all" # all, active, completed
def add(self, text: str): if text.strip(): self.items.append({ "id": len(self.items), "text": text.strip(), "done": False })
def toggle(self, id: int): for item in self.items: if item["id"] == id: item["done"] = not item["done"] break
def delete(self, id: int): self.items = [item for item in self.items if item["id"] != id]
def set_filter(self, f: str): self.filter = f
def get_filtered_items(self): if self.filter == "active": return [i for i in self.items if not i["done"]] elif self.filter == "completed": return [i for i in self.items if i["done"]] return self.items
def get_remaining_count(self): return len([i for i in self.items if not i["done"]])
state = TodoState()
@ft.componentdef AddTodoForm(): input_value, set_input = ft.use_state("")
def add_todo(_): state.add(input_value) set_input("")
return ft.Row([ ft.TextField( value=input_value, hint_text="Nhập việc cần làm...", expand=True, on_change=lambda e: set_input(e.control.value), on_submit=add_todo, prefix_icon=ft.Icons.ADD_TASK, ), ft.FilledButton("Thêm", icon=ft.Icons.ADD, on_click=add_todo), ])
@ft.componentdef FilterTabs(): return ft.Row([ ft.Text(f"{state.get_remaining_count()} việc còn lại", color=ft.Colors.GREY_600, size=14), ft.Container(expand=True), ft.SegmentedButton( selected={state.filter}, on_change=lambda e: state.set_filter(list(e.control.selected)[0]), segments=[ ft.Segment(value="all", label=ft.Text("Tất cả")), ft.Segment(value="active", label=ft.Text("Đang làm")), ft.Segment(value="completed", label=ft.Text("Xong")), ], ), ])
@ft.componentdef TodoItem(item): return ft.ListTile( leading=ft.Checkbox( value=item["done"], on_change=lambda _: state.toggle(item["id"]), ), title=ft.Text( item["text"], style=ft.TextStyle( decoration=ft.TextDecoration.LINE_THROUGH if item["done"] else None, color=ft.Colors.GREY_500 if item["done"] else None, ), ), trailing=ft.IconButton( ft.Icons.DELETE_OUTLINE, icon_color=ft.Colors.RED_300, tooltip="Xóa", on_click=lambda _: state.delete(item["id"]), ), )
@ft.componentdef TodoList(): filtered = state.get_filtered_items()
if not filtered: msg = "✨ Không có việc nào" if state.filter != "all" else "✨ Thêm việc cần làm!" return ft.Container( content=ft.Text(msg, color=ft.Colors.GREY_500, size=16), padding=40, alignment=ft.alignment.center, )
return ft.Column([TodoItem(item) for item in filtered])
@ft.componentdef TodoApp(): return ft.Container( content=ft.Column([ ft.Text("📝 Todo List", size=28, weight=ft.FontWeight.BOLD), AddTodoForm(), ft.Container(height=10), FilterTabs(), ft.Divider(), TodoList(), ]), padding=20, width=500, )
def main(page: ft.Page): page.title = "Todo App" page.vertical_alignment = ft.MainAxisAlignment.START page.horizontal_alignment = ft.CrossAxisAlignment.CENTER page.bgcolor = ft.Colors.GREY_100 page.add( ft.Container( content=TodoApp(), bgcolor=ft.Colors.WHITE, border_radius=10, shadow=ft.BoxShadow(blur_radius=10, color=ft.Colors.BLACK12), margin=ft.margin.only(top=40), ) )
ft.run(main)✅ Kết quả
Phần tiêu đề “✅ Kết quả”Ứng dụng Todo hoàn chỉnh với:
- ✅ Thêm task mới
- ✅ Toggle hoàn thành
- ✅ Xóa task
- ✅ Đếm task còn lại
- ✅ Lọc theo trạng thái
- ✅ UI đẹp với Card và shadows
🎯 Thử thách mở rộng
Phần tiêu đề “🎯 Thử thách mở rộng”- Edit task - Cho phép sửa nội dung task
- Due date - Thêm ngày hết hạn
- Priority - Ưu tiên cao/trung bình/thấp
- Persistence - Lưu vào local storage
📝 Kiến thức ôn tập
Phần tiêu đề “📝 Kiến thức ôn tập”@ft.observable- State toàn app@ft.component- Components tái sử dụng- State methods - add, toggle, delete, filter
- Conditional rendering - Hiển thị theo điều kiện
⏭️ Tiếp theo
Phần tiêu đề “⏭️ Tiếp theo”Tuyệt! Tiếp tục với Mini Project: Calculator.