Project: Todo App
សាងសង់ Todo App
ក្នុងមេរៀននេះ យើងនឹងបង្កើត Todo App ពេញលេញមួយដោយប្រើ Flutter។ Project នេះនឹងរួមបញ្ចូលគ្រប់អ្វីដែលយើងបានរៀន៖ widgets, state management, local storage, navigation, និង forms។
�️ Project Architecture Theory
App Architecture Pattern:
Todo App Architecture (MVC-like)
┌─────────────────────────────────────┐
│ Presentation Layer │
│ (Screens & Widgets - UI) │
│ - HomeScreen (displays todos) │
│ - AddTodoScreen (form input) │
│ - TodoItem widget (reusable) │
└─────────┬───────────────────────────┘
│
↓ User Actions (add, delete, toggle)
│
┌─────────┴───────────────────────────┐
│ Business Logic Layer │
│ (State Management in State class) │
│ - todos list state │
│ - addTodo(), deleteTodo() │
│ - toggleTodo() │
│ - Validation logic │
└─────────┬───────────────────────────┘
│
↓ Data Operations (save/load)
│
┌─────────┴───────────────────────────┐
│ Data Layer │
│ (Persistence - SharedPreferences) │
│ - saveTodos() │
│ - loadTodos() │
│ - JSON serialization │
└─────────────────────────────────────┘
Why This Pattern?
✅ Separation of Concerns
✅ Easy to test each layer
✅ Reusable components
✅ Maintainable code
State Management Approach:
Component | State Type | Why? |
---|---|---|
HomeScreen | Stateful (with setState) | Manages todo list state, needs to rebuild on changes |
AddTodoScreen | Stateful (Form state) | Manages form input, validation state |
TodoItem | Stateless | Just displays data, receives callbacks from parent |
Todo Model | Plain Dart class | Data structure with serialization methods |
Data Flow:
CRUD Operations Flow
CREATE (Add Todo):
1. User fills form in AddTodoScreen
2. Validates input
3. Creates Todo object with unique ID
4. Returns to HomeScreen via Navigator.pop()
5. HomeScreen adds to todos list
6. Calls saveTodos() → SharedPreferences
7. setState() triggers UI rebuild
READ (Load Todos):
1. HomeScreen initState() called
2. loadTodos() reads from SharedPreferences
3. JSON string → List via fromJson()
4. setState() updates todos list
5. ListView.builder displays items
UPDATE (Toggle Complete):
1. User taps checkbox in TodoItem
2. Callback onToggle() called
3. HomeScreen finds todo by ID
4. Toggles isCompleted boolean
5. saveTodos() persists change
6. setState() rebuilds UI
DELETE (Remove Todo):
1. User taps delete icon
2. Callback onDelete() called
3. HomeScreen removes from list by ID
4. saveTodos() updates storage
5. setState() rebuilds UI
�🎯 មុខងារ (Features)
- ✅ Create: បន្ថែម task ថ្មីជាមួយ title និង description
- ✅ Read: បង្ហាញ tasks ទាំងអស់ក្នុង list view
- ✅ Update: សម្គាល់ task ជាបញ្ចប់ (toggle checkbox)
- ✅ Delete: លុប task ចេញពី list
- 💾 Persistence: រក្សាទុកក្នុងឧបករណ៍ (SharedPreferences)
- 🔄 Auto-save: រក្សាទុកស្វ័យប្រវត្តិនៅពេល CRUD operation
📁 រចនាសម្ព័ន្ធ Project (Clean Architecture)
lib/
├── main.dart # App entry point
├── models/ # Data models
│ └── todo.dart # Todo class with JSON methods
├── screens/ # Full-screen pages
│ ├── home_screen.dart # Main todo list screen
│ └── add_todo_screen.dart # Add/edit form
└── widgets/ # Reusable components
└── todo_item.dart # Single todo card widget
Why This Structure?
✅ Models: Reusable data structures
✅ Screens: Complete pages with navigation
✅ Widgets: Small, reusable UI components
✅ Easy to find and modify code
✅ Scalable for larger projects
📝 Todo Model (Data Layer)
Model Design Decisions:
Field | Type | Purpose |
---|---|---|
id |
String | Unique identifier (using timestamp) |
title |
String | Required main task description |
description |
String | Optional detailed notes |
isCompleted |
bool | Track completion status |
createdAt |
DateTime | Sort tasks by creation time |
// models/todo.dart
class Todo {
String id;
String title;
String description;
bool isCompleted;
DateTime createdAt;
Todo({
required this.id,
required this.title,
this.description = '',
this.isCompleted = false,
required this.createdAt,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'isCompleted': isCompleted,
'createdAt': createdAt.toIso8601String(),
};
}
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'],
title: json['title'],
description: json['description'] ?? '',
isCompleted: json['isCompleted'] ?? false,
createdAt: DateTime.parse(json['createdAt']),
);
}
}
JSON Serialization Pattern:
// Why toJson() and fromJson()?
// - Convert Dart objects ↔ JSON strings
// - Store in SharedPreferences (only accepts strings)
// - Send to APIs (JSON is standard format)
// Example:
Todo todo = Todo(id: '1', title: 'Learn Flutter', createdAt: DateTime.now());
// To JSON (for storage)
Map<String, dynamic> json = todo.toJson();
// Result: {'id': '1', 'title': 'Learn Flutter', 'createdAt': '2024-01-01T10:00:00'}
// From JSON (when loading)
Todo loadedTodo = Todo.fromJson(json);
// Result: Todo object with all properties restored
🏠 Home Screen (Main Logic)
State Management Strategy:
HomeScreen State Variables:
List<Todo> todos = [];
│
├── Why List? Can add, remove, iterate
├── Why not Set? Need to maintain order
├── Why State? Changes on CRUD operations
└── Initialized empty, loaded in initState()
Key Methods:
1. loadTodos() - Async load from storage
2. saveTodos() - Async save to storage
3. addTodo() - Add + save + setState
4. toggleTodo() - Update + save + setState
5. deleteTodo() - Remove + save + setState
Pattern: Operation → Save → setState
This ensures UI always matches data!
// screens/home_screen.dart
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
List<Todo> todos = [];
@override
void initState() {
super.initState();
loadTodos();
}
Future<void> loadTodos() async {
// Load from SharedPreferences
final prefs = await SharedPreferences.getInstance();
final String? todosJson = prefs.getString('todos');
if (todosJson != null) {
List<dynamic> decoded = jsonDecode(todosJson);
setState(() {
todos = decoded.map((item) => Todo.fromJson(item)).toList();
});
}
}
Future<void> saveTodos() async {
final prefs = await SharedPreferences.getInstance();
final String encoded = jsonEncode(todos.map((t) => t.toJson()).toList());
await prefs.setString('todos', encoded);
}
void addTodo(Todo todo) {
setState(() {
todos.add(todo);
});
saveTodos();
}
void toggleTodo(String id) {
setState(() {
final index = todos.indexWhere((t) => t.id == id);
todos[index].isCompleted = !todos[index].isCompleted;
});
saveTodos();
}
void deleteTodo(String id) {
setState(() {
todos.removeWhere((t) => t.id == id);
});
saveTodos();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todo App'),
),
body: todos.isEmpty
? Center(
child: Text('មិនមាន tasks'),
)
: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return TodoItem(
todo: todo,
onToggle: () => toggleTodo(todo.id),
onDelete: () => deleteTodo(todo.id),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => AddTodoScreen()),
);
if (result != null) {
addTodo(result);
}
},
),
);
}
}
SharedPreferences Pattern:
// Why SharedPreferences?
// ✅ Simple key-value storage
// ✅ Persists across app restarts
// ✅ Perfect for small data (settings, todos)
// ❌ Not for large data (use SQLite instead)
// ❌ Not secure (don't store passwords)
// Storage Flow:
// 1. Get instance: await SharedPreferences.getInstance()
// 2. Read: prefs.getString('key')
// 3. Write: prefs.setString('key', value)
// For complex objects (like List<Todo>):
// 1. Convert to JSON: jsonEncode(list)
// 2. Store as String
// 3. Load as String
// 4. Convert back: jsonDecode(string)
➕ Add Todo Screen (Form Handling)
Form Validation Strategy:
Field | Validation Rule | Error Message |
---|---|---|
Title | Required, not empty | "សូមបញ្ចូលចំណងជើង" |
Description | Optional | - |
Navigation Pattern (Returning Data):
// Pattern: Push screen → Wait for result → Handle result
// 1. Navigate and wait
final result = await Navigator.push(...);
// 2. AddTodoScreen creates Todo object
// 3. Return with data
Navigator.pop(context, todoObject);
// 4. Check result and add
if (result != null) {
addTodo(result);
}
// Why this pattern?
// ✅ Clean separation between screens
// ✅ Parent decides what to do with result
// ✅ Child doesn't need to know parent's state
// ✅ Easy to reuse AddTodoScreen elsewhere
// screens/add_todo_screen.dart
class AddTodoScreen extends StatefulWidget {
@override
_AddTodoScreenState createState() => _AddTodoScreenState();
}
class _AddTodoScreenState extends State<AddTodoScreen> {
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('បន្ថែម Task'),
),
body: Padding(
padding: EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _titleController,
decoration: InputDecoration(
labelText: 'ចំណងជើង',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'សូមបញ្ចូលចំណងជើង';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: InputDecoration(
labelText: 'ពិពណ៌នា',
border: OutlineInputBorder(),
),
maxLines: 3,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
final todo = Todo(
id: DateTime.now().toString(),
title: _titleController.text,
description: _descriptionController.text,
createdAt: DateTime.now(),
);
Navigator.pop(context, todo);
}
},
child: Text('រក្សាទុក'),
),
],
),
),
),
);
}
}
TextEditingController Pattern:
// Why Controllers?
// ✅ Get text value: _controller.text
// ✅ Set initial value: _controller.text = 'default'
// ✅ Clear input: _controller.clear()
// ✅ Listen to changes: _controller.addListener(...)
// Important: Always dispose!
@override
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
}
// Memory leak if you forget dispose()!
📦 Todo Item Widget (Presentation)
Component Design Principles:
TodoItem Widget Design
Why Stateless?
✅ Only displays data (no internal state)
✅ Receives callbacks from parent
✅ Better performance (no setState overhead)
Props Pattern:
- todo: Data to display
- onToggle: Callback for checkbox
- onDelete: Callback for delete button
Why Callbacks?
✅ Parent manages state (single source of truth)
✅ Widget stays dumb and reusable
✅ Easy to test (just check callback calls)
UI Features:
- Checkbox for completion
- Strikethrough text when complete
- Conditional description display
- Delete button with warning color
// widgets/todo_item.dart
class TodoItem extends StatelessWidget {
final Todo todo;
final VoidCallback onToggle;
final VoidCallback onDelete;
const TodoItem({
required this.todo,
required this.onToggle,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => onToggle(),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: null,
),
),
subtitle: todo.description.isNotEmpty
? Text(todo.description)
: null,
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: onDelete,
),
),
);
}
}
🚀 Enhancement Ideas (Next Steps)
Beginner Enhancements:
- ✨ Edit Todo: Long-press to edit existing todo
- 🔍 Search: Add search bar to filter todos
- 📊 Statistics: Show completed vs pending count
- 🎨 Themes: Add dark/light mode toggle
Intermediate Enhancements:
- 📁 Categories: Group todos by category (Work, Personal, etc.)
- 📅 Due Dates: Add date picker for deadlines
- 🔔 Notifications: Remind users of pending tasks
- ⚡ Priority Levels: High/Medium/Low with color coding
- ↕️ Drag to Reorder: ReorderableListView for sorting
Advanced Enhancements:
- ☁️ Cloud Sync: Firebase Firestore for multi-device sync
- 👥 Sharing: Share todos with other users
- 📎 Attachments: Add images/files to todos
- 🔐 Authentication: User accounts with Firebase Auth
- 📈 Analytics: Track completion rates over time
- 🌐 Offline First: Sync when back online
Example: Adding Categories
// 1. Update Todo Model
class Todo {
String id;
String title;
String description;
bool isCompleted;
DateTime createdAt;
String category; // New field
Todo({
required this.id,
required this.title,
this.description = '',
this.isCompleted = false,
required this.createdAt,
this.category = 'Personal', // Default
});
// Update toJson() and fromJson() to include category
}
// 2. Add Category Dropdown in AddTodoScreen
DropdownButtonFormField<String>(
value: selectedCategory,
items: ['Personal', 'Work', 'Shopping', 'Health']
.map((cat) => DropdownMenuItem(value: cat, child: Text(cat)))
.toList(),
onChanged: (value) {
setState(() {
selectedCategory = value!;
});
},
)
// 3. Group Todos by Category in HomeScreen
Map<String, List<Todo>> groupedTodos = {};
for (var todo in todos) {
if (!groupedTodos.containsKey(todo.category)) {
groupedTodos[todo.category] = [];
}
groupedTodos[todo.category]!.add(todo);
}
// 4. Display with Section Headers
ListView(
children: groupedTodos.entries.map((entry) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16),
child: Text(
entry.key,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
...entry.value.map((todo) => TodoItem(todo: todo)),
],
);
}).toList(),
)
📚 Key Learning Points
💡 Architecture Lessons:
- ✅ Separation of Concerns: Models, Screens, Widgets in different files
- ✅ State Lifting: Parent (HomeScreen) manages state, children receive callbacks
- ✅ Single Source of Truth: todos list in HomeScreen is the only state
- ✅ Persistence Pattern: Load on init, save after every change
- ✅ Navigation with Data: Push → Wait → Pop with result
💡 Best Practices Applied:
- ✅ Model Serialization: toJson/fromJson for data persistence
- ✅ Async Handling: Proper use of async/await for storage
- ✅ Form Validation: User-friendly error messages
- ✅ Stateless When Possible: TodoItem is stateless (performance!)
- ✅ Unique IDs: Using timestamps prevents conflicts
- ✅ Empty State UI: Show message when no todos
- ✅ Dispose Controllers: Prevent memory leaks
⚠️ Common Mistakes to Avoid:
- ❌ Forgetting to call saveTodos() after state changes (data loss!)
- ❌ Not checking null before using SharedPreferences data
- ❌ Using setState in child widget (use callbacks instead)
- ❌ Not disposing TextEditingControllers (memory leaks)
- ❌ Hardcoding IDs (use dynamic IDs like timestamps/UUIDs)
- ❌ No empty state handling (confusing when list is empty)
🎯 Testing Checklist
Test Case | Expected Result |
---|---|
Add todo with title only | ✅ Todo appears in list |
Add todo without title | ❌ Validation error shown |
Toggle todo complete | ✅ Checkbox checked, text strikethrough |
Delete todo | ✅ Todo removed from list |
Close and reopen app | ✅ Todos persist (loaded from storage) |
Add multiple todos | ✅ All appear in order |
Cancel add todo | ✅ No todo added |
✅ លទ្ធផល: អ្នកបានបង្កើត Todo App ពេញលេញមួយដោយប្រើ Clean Architecture! Project នេះបង្ហាញពី state management, data persistence, navigation, forms, និង widget composition។ សាកល្បងបន្ថែម features ផ្សេងទៀតដូចជា categories, due dates, priorities, search, themes, cloud sync។ នេះជា foundation ល្អសម្រាប់បង្កើត production apps!