Flutter
Tutorial Flutter Lanjutan
Materi lanjutan Flutter: struktur folder, repository, state management, CRUD, local storage, testing, build, dan publish.
Tutorial Flutter Lanjutan untuk Pemula yang Naik Level
Materi ini adalah kelanjutan dari Tutorial Flutter Dasar untuk Pemula. Jika materi dasar fokus pada widget, layout, state sederhana, navigasi, form, list, API, dan mini project, maka materi lanjutan ini fokus pada cara membuat project Flutter yang lebih rapi, mudah dirawat, dan lebih dekat ke kebutuhan aplikasi nyata.
Target belajar:
- Memahami struktur folder yang lebih scalable.
- Memisahkan UI, model, service, repository, dan state.
- Mengelola loading, success, empty, dan error state.
- Memahami state management dasar dengan
ValueNotifier, Provider, atau Riverpod secara konseptual. - Membuat alur API yang lebih rapi.
- Menyimpan data lokal dengan pola yang aman.
- Menambahkan validasi form yang lebih baik.
- Mengenal testing, build, dan publish.
Daftar Isi
- Kapan Harus Naik dari Dasar ke Lanjutan
- Mindset Aplikasi Flutter yang Rapi
- Struktur Folder yang Lebih Scalable
- Memisahkan Widget Menjadi Komponen Kecil
- Model Data dan JSON Parsing yang Lebih Aman
- Service dan Repository
- Loading, Empty, Error, dan Success State
- State Management: Dari setState ke Pola yang Lebih Rapi
- Form Lanjutan dan Validasi
- Navigasi dengan Named Routes
- Local Storage dan Cache Sederhana
- API CRUD Sederhana
- Theme, Dark Mode, dan Design System Kecil
- Error Handling yang Enak untuk User
- Testing Dasar
- Build dan Persiapan Publish
- Mini Project Lanjutan: Task Manager
- Roadmap Setelah Materi Ini
1. Kapan Harus Naik dari Dasar ke Lanjutan
Kamu siap masuk materi lanjutan jika sudah bisa:
- Membuat project Flutter baru.
- Membuat
StatelessWidgetdanStatefulWidget. - Menggunakan
setState. - Membuat layout dengan
Column,Row,Container,Padding, danExpanded. - Membaca input dari
TextField. - Menampilkan list dengan
ListView.builder. - Membuka halaman baru dengan
Navigator.push. - Mengambil data API sederhana dengan
FutureBuilder.
Tanda kamu mulai butuh pola lanjutan:
- File
main.dartsudah terlalu panjang. - Banyak kode UI berulang.
- Bingung menaruh function API di mana.
- State mulai tersebar di banyak halaman.
- Error API tidak ditangani dengan konsisten.
- Sulit menambah fitur tanpa merusak fitur lama.
Tujuan materi lanjutan bukan membuat aplikasi menjadi rumit. Tujuannya adalah membuat aplikasi tetap mudah dipahami saat fitur bertambah.
2. Mindset Aplikasi Flutter yang Rapi
Aplikasi Flutter yang rapi biasanya punya pemisahan tanggung jawab:
- UI menampilkan data dan menerima interaksi user.
- State menyimpan kondisi layar.
- Model mendefinisikan bentuk data.
- Service berbicara dengan API, database, atau plugin.
- Repository menjadi jembatan antara UI/state dan sumber data.
Pola sederhana:
Page -> State/Controller -> Repository -> Service/API
|
Model
Penjelasan:
Pagefokus pada tampilan.State/Controllerfokus pada perubahan kondisi.Repositoryfokus pada aturan ambil/simpan data.Service/APIfokus pada detail teknis request.Modelfokus pada bentuk data.
Saat baru belajar, kamu tidak harus memakai arsitektur besar. Cukup biasakan memisahkan kode sesuai tanggung jawab.
3. Struktur Folder yang Lebih Scalable
Struktur sederhana untuk project menengah:
lib/
main.dart
app.dart
core/
constants/
theme/
utils/
features/
tasks/
data/
task_repository.dart
task_service.dart
models/
task.dart
pages/
task_list_page.dart
task_form_page.dart
widgets/
task_card.dart
shared/
widgets/
Kapan memakai folder features?
- Saat aplikasi punya beberapa fitur besar.
- Misalnya
auth,tasks,profile,settings,products,orders.
Kapan memakai folder shared?
- Untuk komponen yang dipakai banyak fitur.
- Misalnya button, empty state, loading view, app text field.
Contoh pembagian sederhana:
models/: class data.pages/: halaman penuh.widgets/: komponen kecil.data/: repository dan service.core/: konfigurasi umum.
Tips:
- Jangan terlalu cepat membuat banyak folder kosong.
- Tambahkan struktur saat mulai ada kebutuhan.
- Hindari semua kode menumpuk di
main.dart.
4. Memisahkan Widget Menjadi Komponen Kecil
Jika satu halaman sudah terlalu panjang, pecah menjadi widget kecil.
Sebelum dipisah:
Card(
child: ListTile(
title: Text(task.title),
subtitle: Text(task.description),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: onDelete,
),
),
)
Setelah dipisah:
class TaskCard extends StatelessWidget {
final Task task;
final VoidCallback onDelete;
const TaskCard({
super.key,
required this.task,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
title: Text(task.title),
subtitle: Text(task.description),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: onDelete,
),
),
);
}
}
Manfaat:
- Halaman lebih pendek.
- Komponen bisa dipakai ulang.
- Lebih mudah dites.
- Lebih mudah dibaca oleh developer lain.
Aturan praktis:
- Jika widget punya nama yang jelas, pisahkan.
- Jika widget dipakai lebih dari sekali, pisahkan.
- Jika method
build()terlalu panjang, pisahkan.
5. Model Data dan JSON Parsing yang Lebih Aman
Model membantu aplikasi memahami bentuk data.
class Task {
final int id;
final String title;
final String description;
final bool isDone;
const Task({
required this.id,
required this.title,
required this.description,
required this.isDone,
});
factory Task.fromJson(Map<String, dynamic> json) {
return Task(
id: json['id'] as int,
title: json['title'] as String? ?? '',
description: json['description'] as String? ?? '',
isDone: json['isDone'] as bool? ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'isDone': isDone,
};
}
}
Kenapa as String? ?? '' lebih aman?
- API kadang mengirim nilai kosong.
- Field bisa hilang.
- Aplikasi tidak langsung crash saat data kurang lengkap.
Namun jangan diam-diam mengabaikan semua error. Untuk aplikasi produksi, tetap log error dan validasi data penting.
6. Service dan Repository
Service fokus pada detail teknis. Repository fokus pada kebutuhan aplikasi.
Contoh service:
class TaskService {
Future<List<Map<String, dynamic>>> fetchTasks() async {
// Request API dilakukan di sini.
return [];
}
}
Contoh repository:
class TaskRepository {
final TaskService service;
TaskRepository(this.service);
Future<List<Task>> getTasks() async {
final jsonList = await service.fetchTasks();
return jsonList.map(Task.fromJson).toList();
}
}
Manfaat repository:
- UI tidak perlu tahu detail API.
- Kalau sumber data berubah, UI tidak perlu banyak berubah.
- Lebih mudah membuat mock data untuk testing.
Pola pemanggilan:
Page meminta data -> Repository mengambil data -> Service request API
7. Loading, Empty, Error, dan Success State
Aplikasi yang baik tidak hanya menampilkan data. Ia juga harus menjelaskan kondisi layar.
Empat state umum:
- Loading: data sedang diambil.
- Empty: data berhasil diambil, tetapi kosong.
- Error: data gagal diambil.
- Success: data berhasil ditampilkan.
Contoh enum:
enum ViewStatus {
initial,
loading,
success,
empty,
error,
}
Contoh pemakaian:
Widget buildBody() {
switch (status) {
case ViewStatus.loading:
return const Center(child: CircularProgressIndicator());
case ViewStatus.empty:
return const Center(child: Text('Belum ada data'));
case ViewStatus.error:
return Center(
child: ElevatedButton(
onPressed: fetchData,
child: const Text('Coba Lagi'),
),
);
case ViewStatus.success:
return TaskList(tasks: tasks);
case ViewStatus.initial:
return const SizedBox.shrink();
}
}
Ini membuat UI lebih jelas dan mudah dirawat.
8. State Management: Dari setState ke Pola yang Lebih Rapi
setState cukup untuk:
- Counter.
- Form sederhana.
- Toggle kecil.
- Halaman dengan state lokal.
Namun saat state dipakai banyak widget atau halaman, gunakan pola yang lebih rapi.
Pilihan bertahap:
setStateValueNotifier- Provider
- Riverpod
- Bloc/Cubit
ValueNotifier
ValueNotifier cocok untuk state sederhana yang ingin dipisahkan dari widget.
final counter = ValueNotifier<int>(0);
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, child) {
return Text('Nilai: $value');
},
)
Ubah nilai:
counter.value++;
Provider atau Riverpod
Provider dan Riverpod membantu membagikan state ke banyak widget.
Contoh konsep:
Widget membaca state -> Controller mengubah state -> UI otomatis update
Untuk pemula, pahami dulu:
- Apa data yang berubah?
- Siapa yang boleh mengubah data itu?
- Widget mana yang perlu membaca data?
Jangan memilih library hanya karena populer. Pilih saat masalahnya memang butuh.
9. Form Lanjutan dan Validasi
Untuk form yang lebih serius, gunakan Form dan TextFormField.
class TaskFormPage extends StatefulWidget {
const TaskFormPage({super.key});
@override
State<TaskFormPage> createState() => _TaskFormPageState();
}
class _TaskFormPageState extends State<TaskFormPage> {
final formKey = GlobalKey<FormState>();
final titleController = TextEditingController();
@override
void dispose() {
titleController.dispose();
super.dispose();
}
void save() {
if (!formKey.currentState!.validate()) return;
final title = titleController.text.trim();
Navigator.pop(context, title);
}
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: titleController,
decoration: const InputDecoration(labelText: 'Judul'),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Judul wajib diisi';
}
if (value.trim().length < 3) {
return 'Judul minimal 3 karakter';
}
return null;
},
),
ElevatedButton(
onPressed: save,
child: const Text('Simpan'),
),
],
),
);
}
}
Form yang baik:
- Memberi pesan error yang jelas.
- Tidak membuat user menebak.
- Mencegah submit saat data belum valid.
- Membersihkan controller dengan
dispose.
10. Navigasi dengan Named Routes
Untuk aplikasi kecil, MaterialPageRoute sudah cukup. Untuk aplikasi yang mulai punya banyak halaman, named routes bisa membantu.
class AppRoutes {
static const home = '/';
static const taskForm = '/task-form';
static const settings = '/settings';
}
Di MaterialApp:
MaterialApp(
initialRoute: AppRoutes.home,
routes: {
AppRoutes.home: (context) => const TaskListPage(),
AppRoutes.taskForm: (context) => const TaskFormPage(),
AppRoutes.settings: (context) => const SettingsPage(),
},
)
Membuka halaman:
Navigator.pushNamed(context, AppRoutes.taskForm);
Kelebihan:
- Nama route lebih konsisten.
- Navigasi lebih mudah dilacak.
- Cocok untuk project yang mulai banyak halaman.
Untuk aplikasi besar, kamu bisa belajar router package seperti go_router.
11. Local Storage dan Cache Sederhana
Local storage dipakai untuk menyimpan data di device.
Contoh kebutuhan:
- Status login.
- Preferensi theme.
- Data cache kecil.
- Draft form.
Gunakan penyimpanan sesuai kebutuhan:
shared_preferences: data kecil dan sederhana.- secure storage: data sensitif seperti token.
- SQLite/Hive/Isar: data lokal yang lebih kompleks.
Contoh cache sederhana:
class SettingsRepository {
Future<void> saveDarkMode(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('dark_mode', value);
}
Future<bool> getDarkMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('dark_mode') ?? false;
}
}
Catatan penting:
- Jangan simpan password langsung.
- Jangan menganggap cache selalu benar.
- Tetap siapkan fallback jika data lokal rusak atau kosong.
12. API CRUD Sederhana
CRUD adalah:
- Create: membuat data.
- Read: membaca data.
- Update: mengubah data.
- Delete: menghapus data.
Contoh kontrak repository:
abstract class TaskRepository {
Future<List<Task>> getTasks();
Future<Task> createTask(Task task);
Future<Task> updateTask(Task task);
Future<void> deleteTask(int id);
}
Kenapa membuat kontrak seperti ini?
- Lebih jelas fitur data yang tersedia.
- Mudah membuat versi fake untuk testing.
- UI tidak bergantung langsung pada API.
Contoh alur create:
User isi form -> validasi -> repository.createTask -> API POST -> refresh list
Contoh alur delete:
User tap delete -> konfirmasi -> repository.deleteTask -> API DELETE -> hapus dari list
Selalu tangani:
- Loading saat request berjalan.
- Error saat gagal.
- Empty state setelah data kosong.
- Feedback sukses seperti SnackBar.
13. Theme, Dark Mode, dan Design System Kecil
Design system kecil membantu UI tetap konsisten.
Contoh file warna:
class AppColors {
static const primary = Color(0xFF02569B);
static const success = Color(0xFF22C55E);
static const danger = Color(0xFFEF4444);
}
Contoh theme:
ThemeData lightTheme() {
return ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primary,
),
useMaterial3: true,
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
);
}
Untuk dark mode:
MaterialApp(
theme: lightTheme(),
darkTheme: darkTheme(),
themeMode: ThemeMode.system,
)
Tips:
- Simpan warna utama di satu tempat.
- Buat style button dan input konsisten.
- Hindari warna hardcode bertebaran di banyak widget.
14. Error Handling yang Enak untuk User
Error tidak boleh hanya muncul di console.
Contoh error buruk:
Exception: failed request
Contoh error yang lebih ramah:
Gagal mengambil data. Periksa koneksi internet lalu coba lagi.
Buat mapper error:
String mapErrorMessage(Object error) {
final message = error.toString();
if (message.contains('SocketException')) {
return 'Tidak ada koneksi internet';
}
if (message.contains('timeout')) {
return 'Request terlalu lama. Coba lagi nanti';
}
return 'Terjadi kesalahan. Silakan coba lagi';
}
UI error yang baik biasanya punya:
- Pesan singkat.
- Tombol coba lagi.
- Tidak menyalahkan user.
- Tidak menampilkan stack trace.
15. Testing Dasar
Testing membantu memastikan fitur tetap berjalan setelah kode berubah.
Jenis test:
- Unit test: mengetes function atau class kecil.
- Widget test: mengetes tampilan widget.
- Integration test: mengetes alur aplikasi.
Contoh unit test sederhana:
bool isValidTitle(String value) {
return value.trim().length >= 3;
}
Test:
void main() {
test('title minimal 3 karakter', () {
expect(isValidTitle('ab'), false);
expect(isValidTitle('abc'), true);
});
}
Jalankan:
flutter test
Mulai dari test kecil:
- Validator form.
- JSON parsing model.
- Repository fake.
- Widget empty state.
16. Build dan Persiapan Publish
Sebelum publish, cek:
- App name.
- App icon.
- Splash screen.
- Version number.
- Permission Android/iOS.
- Internet permission jika memakai API.
- Build release.
Build Android APK:
flutter build apk --release
Build Android App Bundle:
flutter build appbundle --release
Build iOS:
flutter build ios --release
Checklist sebelum rilis:
- Tidak ada debug banner.
- Tidak ada print sensitif.
- Error state sudah ramah.
- Loading state jelas.
- Aplikasi dites di device fisik.
- Form tidak bisa submit data kosong.
- App icon dan nama sudah benar.
17. Mini Project Lanjutan: Task Manager
Setelah aplikasi catatan belajar, buat project lanjutan: Task Manager.
Fitur utama:
- Menampilkan daftar task.
- Menambahkan task.
- Mengedit task.
- Menghapus task.
- Menandai task selesai.
- Filter task: semua, aktif, selesai.
- Simpan data lokal.
- Empty state.
- Error state.
- Theme sederhana.
Struktur fitur:
features/tasks/
models/task.dart
data/task_repository.dart
pages/task_list_page.dart
pages/task_form_page.dart
widgets/task_card.dart
widgets/task_filter_bar.dart
Tahap pengerjaan:
- Buat model
Task. - Buat halaman list task.
- Buat form tambah task.
- Tambahkan edit task.
- Tambahkan delete dengan dialog konfirmasi.
- Tambahkan toggle selesai.
- Tambahkan filter.
- Simpan data lokal.
- Rapikan theme.
- Tambahkan test kecil.
Contoh model:
class Task {
final String id;
final String title;
final String description;
final bool isDone;
const Task({
required this.id,
required this.title,
required this.description,
required this.isDone,
});
Task copyWith({
String? title,
String? description,
bool? isDone,
}) {
return Task(
id: id,
title: title ?? this.title,
description: description ?? this.description,
isDone: isDone ?? this.isDone,
);
}
}
Kenapa copyWith penting?
- Membuat object baru tanpa mengubah object lama.
- Cocok untuk state management.
- Lebih aman untuk data immutable.
18. Roadmap Setelah Materi Ini
Setelah memahami materi lanjutan, lanjutkan ke:
-
State management pilihan
- Riverpod
- Bloc/Cubit
- Provider lebih dalam
-
Arsitektur
- Repository pattern
- Clean architecture dasar
- Dependency injection
- Feature-first folder structure
-
Networking lebih kuat
- Interceptor
- Token refresh
- Timeout
- Retry
- Pagination
-
Database lokal
- SQLite
- Hive
- Isar
- Offline-first app
-
UI lanjutan
- Animation
- CustomPaint
- Responsive layout
- Adaptive UI Android/iOS
-
Testing dan CI
- Unit test
- Widget test
- Integration test
- GitHub Actions
-
Production readiness
- Crash reporting
- Analytics
- Logging
- Performance profiling
- App signing
Checklist Penguasaan Lanjutan
- Bisa memisahkan page, widget, model, repository, dan service.
- Bisa membuat model dengan
fromJsondantoJson. - Bisa menangani loading, empty, error, dan success state.
- Bisa membuat form dengan
FormdanTextFormField. - Bisa memakai named routes.
- Bisa menyimpan preferensi sederhana.
- Bisa membuat alur CRUD sederhana.
- Bisa membuat theme kecil yang konsisten.
- Bisa menampilkan pesan error yang ramah.
- Bisa menulis unit test sederhana.
- Bisa menjalankan
flutter test. - Bisa build APK atau App Bundle release.
- Bisa membuat mini project Task Manager.
Jika checklist ini sudah banyak terpenuhi, kamu sudah siap masuk ke topik Flutter intermediate yang lebih serius.
Penutup
Belajar Flutter lanjutan bukan berarti langsung memakai arsitektur yang rumit. Mulailah dari kebiasaan kecil: pecah widget, rapikan folder, pisahkan data layer, tangani semua state layar, dan buat error message yang manusiawi.
Jika materi dasar membuat kamu bisa membangun layar, materi lanjutan ini membuat kamu bisa membangun aplikasi yang lebih tahan tumbuh.