Mini Project: Todo List ✅
Xây dựng ứng dụng Todo List đầy đủ chức năng. ⏱️ 30 phút
🎯 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
Bước 1: Cấu trúc cơ bản
Tạo file todo_app.py:
import flet as ft
@ft.observable
class 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
import flet as ft
@ft.observable
class 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.component
def 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
import flet as ft
@ft.observable
class 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.component
def 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.component
def 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.component
def 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
import flet as ft
@ft.observable
class 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.component
def 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.component
def 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.component
def 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.component
def 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.component
def 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ả
Ứ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
- 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
@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
Tuyệt! Tiếp tục với Mini Project: Calculator.
Last updated on