1. Pendahuluan
Flutter adalah framework open-source yang dikembangkan oleh Google untuk membangun aplikasi multiplatform (Android, iOS, Web, bahkan Desktop) hanya dengan satu basis kode. Salah satu proyek pemula yang paling populer adalah aplikasi To-Do List, karena selain sederhana, juga melatih kita memahami konsep dasar state management, widget, dan interaksi antar-komponen.
Pada artikel ini, kita akan membahas bagaimana cara membuat aplikasi To-Do List sederhana dengan Flutter, menjalankannya di HP Android, dan hanya menggunakan Visual Studio Code (VS Code) sebagai editor.
2. Persiapan
Sebelum mulai coding, pastikan beberapa hal berikut:
2.1 Install Flutter SDK
- Download dari flutter.dev.
- Ekstrak dan tambahkan ke PATH environment.
- Cek instalasi dengan:
flutter doctor 2.2 Install VS Code
Pastikan Install terlebih dahulu extension Flutter dan Dart.
2.3 Android SDK & Emulator / Device Fisik
- Bisa menggunakan HP Android langsung dengan mengaktifkan USB Debugging (di menu Developer Options) di device Android kalian.
- Pastikan HP terdeteksi dengan:
flutter devicesJika device kalian terdeteksi maka akan muncul :

3. Membuat Project Flutter
1.Buka terminal di VS Code lalu jalankan:
flutter create (nama_project)Perintah ini akan membuat folder project flutter yang nanti nya akan kalian gunakan.
2. Masuk ke folder project:
cd todo_app
3. Jalankan project default untuk memastikan sudah berjalan:
flutter run 4. Membuat Struktur Aplikasi To-Do List
- Bersihkan File
main.dart - Buka
lib/main.dartlalu hapus kode bawaan dan ganti dengan kode ini:
/*
Complete Flutter To-Do List App (single-file example)
- File: lib/main.dart
Dependencies (add to pubspec.yaml under dependencies):
flutter:
sdk: flutter
provider: ^6.0.5
sqflite: ^2.2.8+4
path: ^1.8.3
intl: ^0.18.1
uuid: ^3.1.1
How to run:
1. Create a new Flutter project: `flutter create todo_app`
2. Replace lib/main.dart with this file.
3. Add the dependencies above to pubspec.yaml and run `flutter pub get`.
4. Run in VSCode (debug/run) or `flutter run`.
Features implemented:
- Add / Edit / Delete tasks
- Mark complete / incomplete
- Title, description, due date, priority, tags
- Search, filter by status, priority, sort by due date or creation
- Local persistence using sqflite
- Swipe to delete, undo via SnackBar
- Simple statistics (counts)
NOTE: This is a single-file demo for clarity. In production, split into multiple files.
*/
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
import 'package:uuid/uuid.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final db = await DatabaseHelper.instance.database;
runApp(MyApp());
}
// ----------------------------- Models -----------------------------
class Task {
String id;
String title;
String description;
DateTime? dueDate;
int priority; // 0 low, 1 normal, 2 high
bool completed;
List<String> tags;
DateTime createdAt;
Task({
required this.id,
required this.title,
this.description = '',
this.dueDate,
this.priority = 1,
this.completed = false,
List<String>? tags,
DateTime? createdAt,
}) : tags = tags ?? [],
createdAt = createdAt ?? DateTime.now();
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'description': description,
'dueDate': dueDate?.toIso8601String(),
'priority': priority,
'completed': completed ? 1 : 0,
'tags': jsonEncode(tags),
'createdAt': createdAt.toIso8601String(),
};
}
factory Task.fromMap(Map<String, dynamic> m) {
return Task(
id: m['id'],
title: m['title'],
description: m['description'] ?? '',
dueDate: m['dueDate'] != null ? DateTime.tryParse(m['dueDate']) : null,
priority: m['priority'] ?? 1,
completed: (m['completed'] ?? 0) == 1,
tags: (m['tags'] != null && m['tags'] != '')
? List<String>.from(jsonDecode(m['tags']))
: [],
createdAt: m['createdAt'] != null
? DateTime.parse(m['createdAt'])
: DateTime.now(),
);
}
}
// ----------------------------- Database -----------------------------
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('todo.db');
return _database!;
}
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = p.join(dbPath, filePath);
return await openDatabase(path, version: 1, onCreate: _createDB);
}
Future _createDB(Database db, int version) async {
await db.execute('''
CREATE TABLE tasks(
id TEXT PRIMARY KEY,
title TEXT,
description TEXT,
isDone INTEGER,
createdAt TEXT
)
''');
}
Future<void> insertTask(Task task) async {
final db = await instance.database;
await db.insert(
'tasks',
task.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<void> updateTask(Task task) async {
final db = await instance.database;
await db.update(
'tasks',
task.toMap(),
where: 'id = ?',
whereArgs: [task.id],
);
}
Future<void> deleteTask(String id) async {
final db = await instance.database;
await db.delete('tasks', where: 'id = ?', whereArgs: [id]);
}
Future<List<Task>> getAllTasks() async {
final db = await instance.database;
final result = await db.query('tasks');
return result.map((m) => Task.fromMap(m)).toList();
}
}
// ----------------------------- Provider -----------------------------
class TaskProvider with ChangeNotifier {
List<Task> _tasks = [];
List<Task> get tasks => _tasks;
bool _isLoading = true;
bool get isLoading => _isLoading;
TaskProvider() {
loadTasks();
}
Future<void> loadTasks() async {
_isLoading = true;
notifyListeners();
_tasks = await DatabaseHelper.instance.getAllTasks();
// sort by dueDate then createdAt
_tasks.sort((a, b) {
if (a.dueDate == null && b.dueDate == null) {
return a.createdAt.compareTo(b.createdAt);
}
if (a.dueDate == null) return 1;
if (b.dueDate == null) return -1;
return a.dueDate!.compareTo(b.dueDate!);
});
_isLoading = false;
notifyListeners();
}
Future<void> addTask(Task task) async {
_tasks.add(task);
await DatabaseHelper.instance.insertTask(task);
notifyListeners();
}
Future<void> updateTask(Task updated) async {
final idx = _tasks.indexWhere((t) => t.id == updated.id);
if (idx != -1) _tasks[idx] = updated;
await DatabaseHelper.instance.updateTask(updated);
notifyListeners();
}
Future<void> deleteTask(String id) async {
_tasks.removeWhere((t) => t.id == id);
await DatabaseHelper.instance.deleteTask(id);
notifyListeners();
}
Future<void> toggleComplete(String id) async {
final idx = _tasks.indexWhere((t) => t.id == id);
if (idx == -1) return;
final t = _tasks[idx];
t.completed = !t.completed;
await DatabaseHelper.instance.updateTask(t);
notifyListeners();
}
// filtering helpers
List<Task> filter({
String? q,
int? priority,
bool? completed,
String sortBy = 'due',
}) {
var list = _tasks.toList();
if (q != null && q.trim().isNotEmpty) {
final qq = q.toLowerCase();
list = list
.where(
(t) =>
t.title.toLowerCase().contains(qq) ||
t.description.toLowerCase().contains(qq) ||
t.tags.any((tag) => tag.toLowerCase().contains(qq)),
)
.toList();
}
if (priority != null) {
list = list.where((t) => t.priority == priority).toList();
}
if (completed != null) {
list = list.where((t) => t.completed == completed).toList();
}
if (sortBy == 'due') {
list.sort((a, b) {
if (a.dueDate == null && b.dueDate == null) {
return a.createdAt.compareTo(b.createdAt);
}
if (a.dueDate == null) return 1;
if (b.dueDate == null) return -1;
return a.dueDate!.compareTo(b.dueDate!);
});
} else if (sortBy == 'priority') {
list.sort((a, b) => b.priority.compareTo(a.priority));
} else if (sortBy == 'created') {
list.sort((a, b) => b.createdAt.compareTo(a.createdAt));
}
return list;
}
int countCompleted() => _tasks.where((t) => t.completed).length;
int countPending() => _tasks.where((t) => !t.completed).length;
}
// ----------------------------- UI -----------------------------
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TaskProvider(),
child: MaterialApp(
title: 'ToDo App',
theme: ThemeData(primarySwatch: Colors.indigo, useMaterial3: true),
home: HomePage(),
),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _search = '';
int? _filterPriority;
bool? _filterCompleted;
String _sortBy = 'due';
final _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
final prov = Provider.of<TaskProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text('ToDo — lengkap & efisien'),
actions: [
IconButton(
icon: Icon(Icons.settings),
onPressed: () => _openSettings(context),
),
],
bottom: PreferredSize(
preferredSize: Size.fromHeight(88),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
child: Column(
children: [
Row(
children: [
Expanded(child: _buildSearchField()),
SizedBox(width: 8),
ElevatedButton.icon(
icon: Icon(Icons.filter_list),
label: Text('Filter'),
onPressed: () => _openFilterDialog(),
),
],
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Sort: ${_sortBy == 'due'
? 'Due date'
: _sortBy == 'priority'
? 'Priority'
: 'Created'}',
),
Wrap(
spacing: 8,
children: [
ChoiceChip(
label: Text('Due'),
selected: _sortBy == 'due',
onSelected: (_) => setState(() => _sortBy = 'due'),
),
ChoiceChip(
label: Text('Priority'),
selected: _sortBy == 'priority',
onSelected: (_) =>
setState(() => _sortBy = 'priority'),
),
ChoiceChip(
label: Text('Created'),
selected: _sortBy == 'created',
onSelected: (_) =>
setState(() => _sortBy = 'created'),
),
],
),
],
),
],
),
),
),
),
body: prov.isLoading
? Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: () => prov.loadTasks(),
child: Column(
children: [
_buildStats(prov),
Expanded(child: _buildList(prov)),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _openTaskEditor(context),
child: Icon(Icons.add),
),
);
}
Widget _buildSearchField() {
return TextField(
decoration: InputDecoration(
hintText: 'Cari judul, deskripsi, atau tag...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 0),
),
onChanged: (v) => setState(() => _search = v),
);
}
Widget _buildStats(TaskProvider prov) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_statCard('Total', prov.tasks.length.toString(), Icons.list),
_statCard(
'Selesai',
prov.countCompleted().toString(),
Icons.check_circle,
),
_statCard('Pending', prov.countPending().toString(), Icons.pending),
],
),
);
}
Widget _statCard(String title, String value, IconData icon) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12),
child: Column(
children: [
Icon(icon),
SizedBox(height: 6),
Text(
value,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(title),
],
),
),
);
}
Widget _buildList(TaskProvider prov) {
final items = prov.filter(
q: _search,
priority: _filterPriority,
completed: _filterCompleted,
sortBy: _sortBy,
);
if (items.isEmpty) {
return ListView(
physics: AlwaysScrollableScrollPhysics(),
children: [
SizedBox(height: 120),
Icon(Icons.inbox, size: 80, color: Colors.grey[300]),
SizedBox(height: 12),
Center(
child: Text(
'Belum ada tugas sesuai filter',
style: TextStyle(color: Colors.grey[600]),
),
),
],
);
}
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, idx) {
final t = items[idx];
return Dismissible(
key: Key(t.id),
background: Container(
color: Colors.red,
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: 20),
child: Icon(Icons.delete, color: Colors.white),
),
secondaryBackground: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20),
child: Icon(Icons.delete, color: Colors.white),
),
onDismissed: (direction) async {
final prov = Provider.of<TaskProvider>(context, listen: false);
await prov.deleteTask(t.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Tugas dihapus'),
action: SnackBarAction(
label: 'Undo',
onPressed: () async {
await prov.addTask(t);
},
),
),
);
},
child: Card(
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: Checkbox(
value: t.completed,
onChanged: (_) => Provider.of<TaskProvider>(
context,
listen: false,
).toggleComplete(t.id),
),
title: Text(
t.title,
style: TextStyle(
decoration: t.completed ? TextDecoration.lineThrough : null,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (t.description.isNotEmpty)
Text(
t.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4),
Row(
children: [
if (t.dueDate != null)
Row(
children: [
Icon(Icons.calendar_today, size: 14),
SizedBox(width: 4),
Text(
DateFormat.yMMMd().add_jm().format(t.dueDate!),
),
],
),
SizedBox(width: 8),
_priorityChip(t.priority),
SizedBox(width: 6),
...t.tags
.take(3)
.map(
(tag) => Padding(
padding: EdgeInsets.only(left: 6),
child: Chip(
label: Text(
tag,
style: TextStyle(fontSize: 12),
),
),
),
),
],
),
],
),
trailing: IconButton(
icon: Icon(Icons.edit),
onPressed: () => _openTaskEditor(context, task: t),
),
),
),
);
},
);
}
Widget _priorityChip(int p) {
String label = p == 2
? 'High'
: p == 1
? 'Normal'
: 'Low';
Color color = p == 2
? Colors.red
: p == 1
? Colors.orange
: Colors.green;
return Container(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
),
child: Text(label, style: TextStyle(color: color)),
);
}
void _openTaskEditor(BuildContext context, {Task? task}) {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (_) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: TaskEditor(task: task),
),
);
}
void _openFilterDialog() {
showDialog(
context: context,
builder: (ctx) {
int? tmpPriority = _filterPriority;
bool? tmpCompleted = _filterCompleted;
return AlertDialog(
title: Text('Filter'),
content: StatefulBuilder(
builder: (context, setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButton<int?>(
value: tmpPriority,
hint: Text('Pilih priority (semua)'),
isExpanded: true,
items: [null, 0, 1, 2]
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(
e == null
? 'Semua'
: e == 2
? 'High'
: e == 1
? 'Normal'
: 'Low',
),
),
)
.toList(),
onChanged: (v) => setState(() => tmpPriority = v),
),
SizedBox(height: 8),
DropdownButton<bool?>(
value: tmpCompleted,
isExpanded: true,
items: [null, true, false]
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(
e == null
? 'Semua status'
: e
? 'Selesai'
: 'Pending',
),
),
)
.toList(),
onChanged: (v) => setState(() => tmpCompleted = v),
),
],
);
},
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('Batal'),
),
ElevatedButton(
onPressed: () {
setState(() {
_filterPriority = tmpPriority;
_filterCompleted = tmpCompleted;
});
Navigator.pop(ctx);
},
child: Text('Terapkan'),
),
],
);
},
);
}
void _openSettings(BuildContext context) {
showModalBottomSheet(context: context, builder: (_) => SettingsSheet());
}
}
class SettingsSheet extends StatelessWidget {
const SettingsSheet({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pengaturan',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 12),
ListTile(
leading: Icon(Icons.info),
title: Text('Tentang'),
subtitle: Text(
'Aplikasi ToDo demo - local persistence menggunakan sqlite',
),
),
ListTile(
leading: Icon(Icons.restore),
title: Text('Reset semua data'),
onTap: () async {
final prov = Provider.of<TaskProvider>(context, listen: false);
final all = prov.tasks.toList();
for (var t in all) {
await prov.deleteTask(t.id);
}
Navigator.pop(context);
},
),
],
),
);
}
}
class TaskEditor extends StatefulWidget {
final Task? task;
const TaskEditor({super.key, this.task});
@override
_TaskEditorState createState() => _TaskEditorState();
}
class _TaskEditorState extends State<TaskEditor> {
final _formKey = GlobalKey<FormState>();
final _titleCtrl = TextEditingController();
final _descCtrl = TextEditingController();
DateTime? _dueDate;
int _priority = 1;
final _tagsCtrl = TextEditingController();
@override
void initState() {
super.initState();
if (widget.task != null) {
_titleCtrl.text = widget.task!.title;
_descCtrl.text = widget.task!.description;
_dueDate = widget.task!.dueDate;
_priority = widget.task!.priority;
_tagsCtrl.text = widget.task!.tags.join(', ');
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
widget.task == null ? 'Tambah Tugas' : 'Edit Tugas',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 12),
TextFormField(
controller: _titleCtrl,
decoration: InputDecoration(
labelText: 'Judul',
border: OutlineInputBorder(),
),
validator: (v) => v == null || v.trim().isEmpty
? 'Judul tidak boleh kosong'
: null,
),
SizedBox(height: 8),
TextFormField(
controller: _descCtrl,
minLines: 2,
maxLines: 5,
decoration: InputDecoration(
labelText: 'Deskripsi',
border: OutlineInputBorder(),
),
),
SizedBox(height: 8),
Row(
children: [
Expanded(
child: InkWell(
onTap: () async {
final now = DateTime.now();
final pickedDate = await showDatePicker(
context: context,
initialDate: _dueDate ?? now,
firstDate: now.subtract(Duration(days: 3650)),
lastDate: now.add(Duration(days: 3650)),
);
if (pickedDate != null) {
final pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(
_dueDate ?? DateTime.now(),
),
);
setState(() {
_dueDate = DateTime(
pickedDate.year,
pickedDate.month,
pickedDate.day,
pickedTime?.hour ?? 0,
pickedTime?.minute ?? 0,
);
});
}
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 14,
),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.calendar_today),
SizedBox(width: 8),
Text(
_dueDate == null
? 'Pilih due date (opsional)'
: DateFormat.yMMMd().add_jm().format(
_dueDate!,
),
),
],
),
),
),
),
SizedBox(width: 8),
DropdownButton<int>(
value: _priority,
items: [0, 1, 2]
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(
e == 2
? 'High'
: e == 1
? 'Normal'
: 'Low',
),
),
)
.toList(),
onChanged: (v) => setState(() => _priority = v ?? 1),
),
],
),
SizedBox(height: 8),
TextFormField(
controller: _tagsCtrl,
decoration: InputDecoration(
labelText: 'Tags (pisah dengan koma)',
border: OutlineInputBorder(),
),
),
SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () async {
if (!_formKey.currentState!.validate()) return;
final prov = Provider.of<TaskProvider>(
context,
listen: false,
);
final tags = _tagsCtrl.text
.split(',')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
if (widget.task == null) {
final id = Uuid().v4();
final task = Task(
id: id,
title: _titleCtrl.text.trim(),
description: _descCtrl.text.trim(),
dueDate: _dueDate,
priority: _priority,
tags: tags,
);
await prov.addTask(task);
} else {
final t = widget.task!;
final updated = Task(
id: t.id,
title: _titleCtrl.text.trim(),
description: _descCtrl.text.trim(),
dueDate: _dueDate,
priority: _priority,
completed: t.completed,
tags: tags,
createdAt: t.createdAt,
);
await prov.updateTask(updated);
}
Navigator.pop(context);
},
child: Text(widget.task == null ? 'Tambah' : 'Simpan'),
),
),
if (widget.task != null) SizedBox(width: 8),
if (widget.task != null)
OutlinedButton(
onPressed: () async {
final prov = Provider.of<TaskProvider>(
context,
listen: false,
);
await prov.deleteTask(widget.task!.id);
Navigator.pop(context);
},
child: Text('Hapus'),
),
],
),
],
),
),
),
);
}
@override
void dispose() {
_titleCtrl.dispose();
_descCtrl.dispose();
_tagsCtrl.dispose();
super.dispose();
}
}5. Menjalankan di HP Android
- Hubungkan HP dengan kabel USB.
- Pastikan sudah aktif USB Debugging.
- Jalankan perintah berikut di terminal atau dengan ctrl+f5 :
flutter run 
6. Fitur yang Bisa Ditambahkan
Jika ingin mengembangkan lebih lanjut, bisa menambahkan:
- Checkbox untuk menandai tugas selesai.
- SharedPreferences agar data tetap tersimpan meskipun aplikasi ditutup.
- UI lebih menarik dengan tema gelap (Dark Mode).
7. Kesimpulan
Membuat aplikasi To-Do List dengan Flutter di VS Code adalah cara yang tepat untuk belajar dasar-dasar Flutter, mulai dari widget, state, hingga interaksi pengguna. Dengan langkah-langkah di atas, kamu sudah bisa membuat aplikasi sederhana, menjalankannya langsung di HP, dan menjadikannya sebagai pondasi untuk proyek Flutter yang lebih kompleks.

