Flutter
Upgrade Task Manager dengan Local Storage
Tutorial upgrade Project Task Manager agar data tidak hilang: tambah toJson/fromJson, simpan List<Task> ke shared_preferences, load saat app dibuka, dan update storage saat add/edit/delete.
Upgrade Task Manager dengan Local Storage
Di Project Task Manager sebelumnya, data task masih disimpan di List<Task> biasa. Artinya, data akan hilang saat aplikasi ditutup. Pada materi ini, kita upgrade Task Manager supaya data tetap tersimpan memakai shared_preferences.
Yang akan dipelajari:
- menambahkan
toJsondanfromJsonke modelTask - menyimpan
List<Task>ke local storage - membaca task saat aplikasi dibuka
- update storage saat tambah task
- update storage saat edit task
- update storage saat hapus task
- membuat service agar kode lebih rapi
1. Gambaran Alur
Sebelum local storage:
User tambah task
-> task masuk ke List
-> UI update
-> aplikasi ditutup
-> List hilang
Setelah local storage:
User tambah task
-> task masuk ke List
-> List disimpan ke shared_preferences
-> UI update
-> aplikasi ditutup
-> aplikasi dibuka lagi
-> task dibaca dari shared_preferences
-> UI menampilkan task lama
Prinsip penting:
State di memory tetap dipakai untuk UI.
Local storage dipakai agar data tidak hilang.
Jadi UI tidak membaca langsung ke storage setiap saat. Storage dibaca saat awal, lalu data dimasukkan ke state. Ketika state berubah, storage ikut diperbarui.
2. Install Package
Tambahkan package:
flutter pub add shared_preferences
Lalu:
flutter pub get
Import yang akan dipakai:
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
3. Struktur Folder
Struktur project yang disarankan:
lib/
main.dart
models/
task.dart
pages/
task_list_page.dart
task_form_page.dart
services/
task_storage_service.dart
widgets/
task_card.dart
task_status_chip.dart
File baru yang akan dibuat:
lib/services/task_storage_service.dart
File yang akan diubah:
lib/models/task.dart
lib/pages/task_list_page.dart
4. Update Model Task
Buka file:
lib/models/task.dart
Contoh model awal:
enum TaskStatus {
todo,
progress,
done,
}
class Task {
final int id;
final String title;
final String description;
final TaskStatus status;
final DateTime createdAt;
const Task({
required this.id,
required this.title,
required this.description,
required this.status,
required this.createdAt,
});
}
Tambahkan toJson, fromJson, dan copyWith:
enum TaskStatus {
todo,
progress,
done,
}
extension TaskStatusLabel on TaskStatus {
String get label {
switch (this) {
case TaskStatus.todo:
return 'Todo';
case TaskStatus.progress:
return 'Progress';
case TaskStatus.done:
return 'Done';
}
}
}
class Task {
final int id;
final String title;
final String description;
final TaskStatus status;
final DateTime createdAt;
const Task({
required this.id,
required this.title,
required this.description,
required this.status,
required this.createdAt,
});
Task copyWith({
String? title,
String? description,
TaskStatus? status,
}) {
return Task(
id: id,
title: title ?? this.title,
description: description ?? this.description,
status: status ?? this.status,
createdAt: createdAt,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'status': status.name,
'createdAt': createdAt.toIso8601String(),
};
}
factory Task.fromJson(Map<String, dynamic> json) {
return Task(
id: json['id'] as int,
title: json['title'] as String,
description: json['description'] as String,
status: TaskStatus.values.byName(json['status'] as String),
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
}
Penjelasan:
toJson()mengubah objectTaskmenjadiMap.fromJson()mengubahMapmenjadi objectTask.status.namemenyimpan enum sebagai string, misalnyatodo.TaskStatus.values.byName(...)mengubah string menjadi enum.createdAt.toIso8601String()menyimpan tanggal sebagai string.DateTime.parse(...)mengubah string kembali menjadiDateTime.
5. Kenapa Enum Disimpan sebagai String
Enum tidak bisa langsung disimpan ke JSON.
Kurang tepat:
'status': status,
Lebih aman:
'status': status.name,
Jika status bernilai:
TaskStatus.todo
Maka status.name menghasilkan:
todo
Saat membaca:
TaskStatus.values.byName('todo')
Hasilnya:
TaskStatus.todo
6. Membuat TaskStorageService
Buat file:
lib/services/task_storage_service.dart
Isi:
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/task.dart';
class TaskStorageService {
static const _tasksKey = 'tasks';
Future<void> saveTasks(List<Task> tasks) async {
final prefs = await SharedPreferences.getInstance();
final jsonList = tasks.map((task) => task.toJson()).toList();
final jsonString = jsonEncode(jsonList);
await prefs.setString(_tasksKey, jsonString);
}
Future<List<Task>> loadTasks() async {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_tasksKey);
if (jsonString == null) {
return [];
}
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
.map((item) => Task.fromJson(item as Map<String, dynamic>))
.toList();
}
Future<void> clearTasks() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_tasksKey);
}
}
Penjelasan:
_tasksKeyadalah key untuk menyimpan semua task.saveTasksmengubahList<Task>menjadi JSON string.loadTasksmembaca JSON string lalu mengubahnya menjadiList<Task>.clearTasksmenghapus data task dari storage.
Kenapa dibuat service?
Supaya logic storage tidak bercampur dengan UI. Page cukup memanggil:
storageService.saveTasks(tasks);
storageService.loadTasks();
7. Memahami saveTasks
Bagian penting:
final jsonList = tasks.map((task) => task.toJson()).toList();
Ini mengubah:
List<Task>
menjadi:
List<Map<String, dynamic>>
Lalu:
final jsonString = jsonEncode(jsonList);
Ini mengubah list map menjadi string.
Baru disimpan:
await prefs.setString(_tasksKey, jsonString);
Alur:
List<Task>
-> List<Map>
-> String JSON
-> shared_preferences
8. Memahami loadTasks
Bagian penting:
final jsonString = prefs.getString(_tasksKey);
Jika belum ada data:
if (jsonString == null) {
return [];
}
Jika ada data:
final jsonList = jsonDecode(jsonString) as List<dynamic>;
Lalu ubah setiap item menjadi Task:
return jsonList
.map((item) => Task.fromJson(item as Map<String, dynamic>))
.toList();
Alur:
shared_preferences
-> String JSON
-> List<dynamic>
-> List<Task>
9. Update TaskListPage
Buka file:
lib/pages/task_list_page.dart
Tambahkan service:
final taskStorageService = TaskStorageService();
Tambahkan state loading:
bool isLoading = true;
Contoh struktur:
class _TaskListPageState extends State<TaskListPage> {
final taskStorageService = TaskStorageService();
final List<Task> tasks = [];
TaskStatus? selectedStatus;
String searchQuery = '';
bool isLoading = true;
@override
void initState() {
super.initState();
loadTasks();
}
Future<void> loadTasks() async {
final savedTasks = await taskStorageService.loadTasks();
setState(() {
tasks
..clear()
..addAll(savedTasks);
isLoading = false;
});
}
}
Penjelasan:
initState()dipanggil saat halaman pertama kali dibuat.loadTasks()membaca data dari storage.tasks.clear()menghapus isi list saat ini.tasks.addAll(savedTasks)mengisi list dari storage.isLoading = falsemenandakan proses load selesai.
10. Tampilkan Loading Saat Membaca Storage
Di build, tambahkan kondisi:
if (isLoading) {
return Scaffold(
appBar: AppBar(
title: const Text('Task Manager'),
),
body: const Center(
child: CircularProgressIndicator(),
),
);
}
Tujuannya:
- user tahu aplikasi sedang membaca data
- UI tidak langsung menampilkan empty state sebelum load selesai
Tanpa loading, user bisa melihat “Belum ada task” sebentar walaupun sebenarnya task masih sedang dibaca.
11. Simpan Storage Saat Add Task
Sebelumnya:
setState(() {
tasks.add(result);
});
Ubah menjadi:
setState(() {
tasks.add(result);
});
await taskStorageService.saveTasks(tasks);
Contoh lengkap:
Future<void> openCreatePage() async {
final result = await Navigator.push<Task>(
context,
MaterialPageRoute(
builder: (context) => const TaskFormPage(),
),
);
if (result == null) return;
setState(() {
tasks.add(result);
});
await taskStorageService.saveTasks(tasks);
}
Alur:
Form mengembalikan task
-> task ditambahkan ke List
-> List disimpan ke local storage
12. Simpan Storage Saat Edit Task
Sebelumnya:
setState(() {
tasks[index] = result;
});
Tambahkan save:
setState(() {
tasks[index] = result;
});
await taskStorageService.saveTasks(tasks);
Contoh lengkap:
Future<void> openEditPage(Task task) async {
final result = await Navigator.push<Task>(
context,
MaterialPageRoute(
builder: (context) => TaskFormPage(task: task),
),
);
if (result == null) return;
final index = tasks.indexWhere((item) => item.id == result.id);
if (index == -1) return;
setState(() {
tasks[index] = result;
});
await taskStorageService.saveTasks(tasks);
}
Setiap edit selesai, storage harus ditimpa dengan list terbaru.
13. Simpan Storage Saat Update Status
Sebelumnya:
setState(() {
tasks[index] = tasks[index].copyWith(status: status);
});
Ubah function menjadi async:
Future<void> updateTaskStatus(int id, TaskStatus status) async {
final index = tasks.indexWhere((task) => task.id == id);
if (index == -1) return;
setState(() {
tasks[index] = tasks[index].copyWith(status: status);
});
await taskStorageService.saveTasks(tasks);
}
Pemanggilan:
onStatusChanged: (status) {
updateTaskStatus(task.id, status);
},
Tidak perlu await di UI jika tidak ada aksi setelah update. Tapi function tetap async agar bisa menyimpan data.
14. Simpan Storage Saat Delete Task
Sebelumnya:
setState(() {
tasks.removeWhere((item) => item.id == task.id);
});
Tambahkan save:
setState(() {
tasks.removeWhere((item) => item.id == task.id);
});
await taskStorageService.saveTasks(tasks);
Contoh lengkap:
Future<void> confirmDelete(Task task) async {
final result = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Hapus task?'),
content: Text('Task "${task.title}" akan dihapus.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Batal'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Hapus'),
),
],
);
},
);
if (result != true) return;
setState(() {
tasks.removeWhere((item) => item.id == task.id);
});
await taskStorageService.saveTasks(tasks);
}
Setelah task dihapus dari memory, storage harus ikut diperbarui.
15. Membuat Helper saveCurrentTasks
Agar tidak menulis:
await taskStorageService.saveTasks(tasks);
berulang-ulang, buat helper:
Future<void> saveCurrentTasks() async {
await taskStorageService.saveTasks(tasks);
}
Lalu gunakan:
setState(() {
tasks.add(result);
});
await saveCurrentTasks();
Ini membuat kode lebih mudah dibaca.
16. Contoh TaskListPage Ringkas
Contoh potongan penting:
class _TaskListPageState extends State<TaskListPage> {
final taskStorageService = TaskStorageService();
final List<Task> tasks = [];
TaskStatus? selectedStatus;
String searchQuery = '';
bool isLoading = true;
@override
void initState() {
super.initState();
loadTasks();
}
Future<void> loadTasks() async {
final savedTasks = await taskStorageService.loadTasks();
setState(() {
tasks
..clear()
..addAll(savedTasks);
isLoading = false;
});
}
Future<void> saveCurrentTasks() async {
await taskStorageService.saveTasks(tasks);
}
Future<void> addTask(Task task) async {
setState(() {
tasks.add(task);
});
await saveCurrentTasks();
}
Future<void> updateTask(Task updatedTask) async {
final index = tasks.indexWhere((task) => task.id == updatedTask.id);
if (index == -1) return;
setState(() {
tasks[index] = updatedTask;
});
await saveCurrentTasks();
}
Future<void> deleteTask(int id) async {
setState(() {
tasks.removeWhere((task) => task.id == id);
});
await saveCurrentTasks();
}
}
Dengan pola ini, setiap perubahan data selalu diikuti penyimpanan storage.
17. Testing Manual
Setelah selesai, lakukan test manual:
- Jalankan aplikasi.
- Tambahkan beberapa task.
- Tutup aplikasi.
- Buka lagi.
- Pastikan task masih ada.
- Edit task.
- Tutup dan buka lagi.
- Pastikan hasil edit masih ada.
- Hapus task.
- Tutup dan buka lagi.
- Pastikan task yang dihapus tidak muncul lagi.
Jika semua berhasil, local storage sudah bekerja.
18. Error yang Sering Terjadi
Data tidak muncul setelah app dibuka
Cek:
- apakah
loadTasks()dipanggil diinitState - apakah key storage sama
- apakah
saveTasks()berhasil dipanggil - apakah parsing JSON error
Data muncul saat ditambah, tapi hilang saat restart
Kemungkinan:
- hanya menambah data ke
List - lupa memanggil
saveTasks
Error saat parsing enum
Penyebab:
TaskStatus.values.byName(json['status'] as String)
akan error jika string status tidak cocok.
Pastikan value yang disimpan berasal dari:
status.name
Error saat parsing DateTime
Pastikan tanggal disimpan dengan:
createdAt.toIso8601String()
Dan dibaca dengan:
DateTime.parse(json['createdAt'] as String)
19. Checklist Upgrade
Pastikan sudah selesai:
-
TaskpunyatoJson. -
TaskpunyafromJson. -
TaskStatusdisimpan denganstatus.name. -
DateTimedisimpan dengantoIso8601String. - Ada
TaskStorageService. - Bisa menyimpan
List<Task>ke shared_preferences. - Bisa load task saat app dibuka.
- Ada loading state saat membaca storage.
- Storage update saat add task.
- Storage update saat edit task.
- Storage update saat update status.
- Storage update saat delete task.
- Data tetap ada setelah aplikasi ditutup dan dibuka lagi.
Jika checklist ini aman, Task Manager sudah punya local persistence dasar.
20. Lanjutan Setelah Ini
Setelah memakai shared_preferences, langkah berikutnya:
- Upgrade Task Manager dengan Provider + local storage.
- Upgrade Task Manager dengan Riverpod + local storage.
- Simpan task ke Hive/Isar untuk data lokal yang lebih besar.
- Sinkronkan task ke Firestore.
- Tambahkan login agar task tersimpan per user.
Urutan paling enak:
setState + shared_preferences
-> Provider + shared_preferences
-> Riverpod + shared_preferences
-> Firestore