Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bab 1: Perkenalan Pemrograman Flutter

Selamat datang di bab pertama buku pemrograman Flutter! Bab ini akan menjadi fondasi bagi perjalanan Anda dalam membangun aplikasi multi-platform yang indah dengan Flutter.

Di bab ini, kita akan memulai dari dasar dengan menjawab pertanyaan fundamental: "Apa itu Flutter?". Kita akan menjelajahi sejarah dan evolusinya, dari proyek eksperimental Google hingga menjadi salah satu kerangka kerja UI modern yang paling populer.

Selanjutnya, kita akan masuk ke bagian teknis dengan mempersiapkan lingkungan pengembangan Anda. Bab ini akan memandu Anda melalui:

  • Persyaratan sistem yang dibutuhkan untuk Windows, macOS, dan Linux.
  • Langkah-langkah instalasi Flutter SDK secara rinci.
  • Konfigurasi PATH agar perintah flutter dapat diakses secara global.
  • Penggunaan flutter doctor untuk mendiagnosis dan memastikan instalasi Anda berjalan sempurna.

Tentu saja, proses instalasi tidak selalu berjalan mulus. Oleh karena itu, bab ini juga dilengkapi dengan bagian troubleshooting yang membahas masalah-masalah umum yang sering terjadi beserta solusinya.

Terakhir, kita akan terjun langsung ke dalam kode dengan membuat proyek Flutter pertama Anda. Anda akan belajar tentang struktur direktori sebuah proyek Flutter, cara menjalankan aplikasi di emulator atau perangkat fisik, dan merasakan keajaiban fitur Hot Reload yang memungkinkan Anda melihat perubahan secara instan.

Setelah menyelesaikan bab ini, Anda akan memiliki pemahaman yang kuat tentang apa itu Flutter dan memiliki lingkungan pengembangan yang siap untuk mulai membangun aplikasi. Mari kita mulai!

Bab 1: Perkenalan Pemrograman Flutter

1.1 Apa itu Flutter?

Flutter adalah sebuah kerangka kerja (framework) antarmuka pengguna (UI) sumber terbuka yang dibuat dan dikelola oleh Google. Diluncurkan pertama kali pada tahun 2017, Flutter memungkinkan pengembang untuk membangun aplikasi yang indah dan berkinerja tinggi untuk berbagai platform—termasuk seluler (iOS dan Android), web, dan desktop—dari satu basis kode (single codebase).

Keunggulan utama Flutter terletak pada kemampuannya untuk menghasilkan aplikasi yang dikompilasi secara native, yang berarti aplikasi tersebut berjalan langsung di atas perangkat keras tanpa lapisan interpretasi tambahan. Hal ini memberikan performa yang setara dengan aplikasi yang dibangun menggunakan bahasa pemrograman asli platform tersebut.

Flutter menggunakan bahasa pemrograman Dart, yang juga dikembangkan oleh Google. Dart adalah bahasa modern yang dioptimalkan untuk pengembangan UI, dengan fitur-fitur seperti hot reload yang memungkinkan pengembang melihat perubahan pada kode secara instan tanpa harus memulai ulang aplikasi.

1.2 Sejarah dan Evolusi Flutter

Perjalanan Flutter dimulai sebagai sebuah proyek eksperimental dan telah berkembang pesat menjadi salah satu kerangka kerja lintas platform paling populer.

Awal Mula: Proyek "Sky" (2015)

Sebelum dikenal sebagai Flutter, proyek ini memiliki nama kode "Sky". Diperkenalkan pertama kali pada Dart Developer Summit 2015, tujuan awalnya adalah untuk membangun antarmuka pengguna yang mampu merender grafis dengan kecepatan 120 frame per second (fps) di perangkat Android. Proyek ini menjadi fondasi bagi fitur ikonik Flutter, yaitu hot reload.

Kelahiran Flutter dan Versi Alpha (2017)

Pada Google I/O 2017, proyek "Sky" secara resmi berganti nama menjadi Flutter dan dirilis dalam versi alpha. Pada tahap ini, fokus utama Flutter adalah pengembangan aplikasi seluler untuk Android dan iOS. Rilis ini menarik perhatian komunitas pengembang berkat arsitektur widget-nya yang fleksibel dan performa yang menjanjikan.

Rilis Stabil: Flutter 1.0 (2018)

Tonggak sejarah terpenting terjadi pada 4 Desember 2018, ketika Google merilis Flutter 1.0 di acara Flutter Live. Rilis ini menandai bahwa Flutter telah stabil dan siap digunakan untuk lingkungan produksi. Fitur-fitur utamanya meliputi:

  • Dukungan penuh untuk aplikasi iOS dan Android.
  • Performa setara native berkat mesin render Skia.
  • Fitur hot reload yang matang.
  • Integrasi dengan bahasa pemrograman Dart 2.

Ekspansi ke Multi-Platform: Flutter 2.0 (2021)

Pada Maret 2021, Google meluncurkan Flutter 2.0, sebuah lompatan besar yang membawa Flutter melampaui platform seluler. Rilis ini secara resmi menghadirkan dukungan stabil untuk platform web, serta dukungan beta untuk Windows, macOS, dan Linux. Selain itu, Flutter 2.0 juga memperkenalkan fitur null safety dari bahasa Dart, yang membantu pengembang menulis kode yang lebih aman dan andal.

Kematangan Platform: Flutter 3.0 (2022)

Flutter 3.0, yang dirilis pada Mei 2022, menyempurnakan visi multi-platform Flutter dengan memberikan dukungan stabil untuk semua platform desktop (Windows, macOS, dan Linux). Dengan rilis ini, Flutter secara resmi menjadi kerangka kerja yang memungkinkan pengembang membangun aplikasi untuk enam platform berbeda dari satu basis kode tunggal.

Sejak saat itu, Flutter terus berkembang dengan pembaruan rutin yang berfokus pada peningkatan performa, penambahan widget baru, dan penguatan ekosistemnya yang kaya akan library dan dukungan komunitas.

1.2 Persyaratan Sistem dan Instalasi

Sebelum memulai pengembangan dengan Flutter, langkah pertama adalah memastikan bahwa lingkungan pengembangan Anda memenuhi persyaratan sistem yang dibutuhkan. Setiap sistem operasi memiliki beberapa kebutuhan spesifik yang harus dipenuhi.

Persyaratan Sistem

Berikut adalah rincian persyaratan sistem untuk pengembangan Flutter pada Windows, macOS, dan Linux.

Windows

KategoriPersyaratan Minimum
Sistem OperasiWindows 10 atau yang lebih baru (64-bit).
Ruang Disk1.64 GB (tidak termasuk IDE dan tools lainnya).
Peralatan- Windows PowerShell 5.0 atau lebih baru.
- Git for Windows 2.x atau lebih baru.

macOS

KategoriPersyaratan Minimum
Sistem OperasimacOS, versi 10.15 (Catalina) atau yang lebih baru.
Ruang Disk2.8 GB (tidak termasuk IDE dan tools lainnya).
Peralatan- Git 2.x atau lebih baru.
- Xcode untuk pengembangan aplikasi iOS.

Linux

KategoriPersyaratan Minimum
Sistem OperasiDistribusi Linux 64-bit (contoh: Debian, Ubuntu, Fedora).
Ruang Disk600 MB (tidak termasuk IDE dan tools lainnya).
Peralatan- bash, curl, file, git 2.x, mkdir, rm, unzip, which, xz-utils, zip
- libGLU.so.1 (biasanya tersedia melalui paket mesa).

Catatan Penting:

  • IDE (Integrated Development Environment): Meskipun tidak wajib, sangat disarankan untuk menggunakan IDE seperti Visual Studio Code atau Android Studio dengan plugin Flutter dan Dart untuk pengalaman pengembangan yang lebih baik.
  • Ruang Disk Tambahan: Persyaratan ruang disk di atas hanya untuk Flutter SDK. Anda akan memerlukan ruang tambahan yang signifikan untuk Android SDK, Xcode, dan tools lainnya, yang bisa mencapai 10 GB atau lebih.

Panduan Instalasi Flutter SDK

Berikut adalah panduan langkah demi langkah untuk menginstal Flutter SDK di berbagai sistem operasi.

Langkah 1: Unduh Flutter SDK

  1. Buka situs resmi Flutter di flutter.dev.
  2. Navigasi ke halaman instalasi dan unduh versi stabil terbaru dari Flutter SDK yang sesuai dengan sistem operasi Anda (Windows, macOS, atau Linux).

Langkah 2: Ekstrak File SDK

  • Windows: Ekstrak file .zip yang telah diunduh ke lokasi yang tidak memerlukan hak akses administrator, misalnya C:\src\flutter. Hindari menginstalnya di C:\Program Files\.
  • macOS/Linux: Ekstrak file tar.xz ke direktori pilihan Anda, misalnya ~/development/ atau ~/flutter_sdk/.

Langkah 3: Konfigurasi PATH

Langkah ini penting agar perintah flutter dapat diakses dari terminal mana pun.

  • Windows:

    1. Cari "Environment Variables" di Start Menu dan pilih "Edit the system environment variables".
    2. Klik tombol "Environment Variables...".
    3. Di bawah "User variables", pilih variabel Path dan klik "Edit".
    4. Klik "New" dan tambahkan path lengkap ke direktori bin di dalam folder Flutter Anda (contoh: C:\src\flutter\bin).
    5. Klik "OK" untuk menyimpan perubahan.
  • macOS/Linux:

    1. Buka file konfigurasi shell Anda (misalnya, ~/.zshrc, ~/.bashrc, atau ~/.bash_profile) dengan editor teks. Contoh untuk Zsh: nano ~/.zshrc.
    2. Tambahkan baris berikut di akhir file, sesuaikan dengan path tempat Anda mengekstrak Flutter:
      export PATH="$PATH:[PATH_KE_FLUTTER_ANDA]/bin"
      
      Contoh: `export PATH="$PATH:~/development/flutter/bin"
    3. Simpan file dan muat ulang konfigurasi shell dengan menjalankan source ~/.zshrc (atau file yang sesuai) atau dengan memulai ulang terminal.

Langkah 4: Jalankan flutter doctor

flutter doctor adalah perintah untuk memeriksa lingkungan pengembangan Anda dan melaporkan status instalasi Flutter.

  1. Buka terminal atau Command Prompt baru.
  2. Jalankan perintah berikut:
    flutter doctor
    
  3. Perhatikan outputnya. flutter doctor akan memberikan laporan tentang:
    • Versi Flutter SDK.
    • Status Android toolchain (termasuk Android Studio dan SDK).
    • Status Xcode (di macOS).
    • Perangkat yang terhubung.
    • Plugin IDE yang terinstal.

Jika flutter doctor menemukan masalah (ditandai dengan [!] atau [X]), ikuti instruksi yang disarankan untuk memperbaikinya.

1.3 Troubleshooting Instalasi

Proses instalasi Flutter dan konfigurasinya terkadang dapat menimbulkan beberapa masalah umum. Bab ini akan membahas masalah-masalah tersebut beserta solusinya untuk membantu Anda memulai pengembangan dengan lancar.

Masalah Umum dan Solusi

Berikut adalah beberapa masalah yang sering ditemui saat instalasi Flutter.

1. Perintah flutter Tidak Dikenali

  • Masalah: Setelah instalasi, terminal atau Command Prompt menampilkan pesan seperti flutter: command not found atau 'flutter' is not recognized as an internal or external command.
  • Penyebab: Lokasi direktori bin di dalam folder instalasi Flutter SDK belum ditambahkan ke environment variable PATH sistem Anda.
  • Solusi:
    1. Temukan lokasi folder flutter di sistem Anda.
    2. Salin path lengkap menuju direktori bin di dalamnya (contoh: C:\src\flutter\bin di Windows atau ~/development/flutter/bin di macOS/Linux).
    3. Tambahkan path tersebut ke environment variable PATH Anda.
    4. Tutup dan buka kembali jendela terminal atau IDE Anda agar perubahan dapat diterapkan.

2. flutter doctor Menemukan Masalah

  • Masalah: Perintah flutter doctor menampilkan satu atau lebih tanda seru [! ] atau tanda silang [X] yang menandakan adanya masalah.
  • Penyebab: Ada komponen atau dependensi yang belum terinstal atau terkonfigurasi dengan benar.
  • Solusi: flutter doctor adalah alat diagnostik yang sangat membantu. Perhatikan output yang dihasilkannya dan ikuti instruksi yang disarankan. Beberapa contoh umum:
    • Android toolchain - develop for Android devices: Masalah ini biasanya terkait dengan instalasi Android Studio, Android SDK, atau cmdline-tools. Buka Android Studio, pergi ke SDK Manager, dan pastikan komponen yang dibutuhkan sudah terinstal.
    • Xcode - develop for iOS and macOS: (Khusus macOS) Pastikan Xcode sudah terinstal dari App Store dan command-line tools-nya sudah diatur dengan menjalankan sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.
    • Android license status unknown: Jalankan perintah flutter doctor --android-licenses dan setujui semua lisensi dengan mengetik y.

3. Masalah Terkait Lisensi Android

  • Masalah: flutter doctor mengindikasikan bahwa lisensi Android belum disetujui.
  • Penyebab: Anda belum menyetujui perjanjian lisensi dari Android SDK.
  • Solusi: Jalankan perintah flutter doctor --android-licenses di terminal. Tekan y dan Enter untuk setiap lisensi yang ditampilkan hingga semua lisensi disetujui.

4. Emulator atau Simulator Gagal Berjalan

  • Masalah: Android Emulator atau iOS Simulator tidak dapat dimulai atau langsung tertutup.
  • Penyebab: Bisa disebabkan oleh berbagai hal, mulai dari kurangnya sumber daya sistem, masalah konfigurasi, hingga driver grafis yang usang.
  • Solusi:
    • Android Emulator:
      • Pastikan fitur virtualisasi (seperti Intel HAXM atau AMD-V) sudah diaktifkan di BIOS/UEFI komputer Anda.
      • Di Android Studio, buka AVD Manager dan coba hapus (Wipe Data) virtual device yang bermasalah, atau buat yang baru.
      • Pastikan Anda memiliki ruang disk yang cukup.
    • iOS Simulator: (Khusus macOS)
      • Coba reset simulator dengan memilih Device > Erase All Content and Settings... dari menu simulator.
      • Pastikan versi Xcode Anda kompatibel dengan versi macOS Anda.

Tips Tambahan

  • Jalankan flutter upgrade: Jika Anda mengalami masalah yang tidak terduga, coba perbarui Flutter SDK ke versi terbaru dengan menjalankan flutter upgrade.
  • Gunakan Perintah flutter clean: Terkadang, file build yang lama dapat menyebabkan konflik. Jalankan flutter clean di direktori proyek Anda untuk membersihkannya.
  • Cari Bantuan Komunitas: Jika masalah berlanjut, jangan ragu untuk mencari solusi di platform seperti Stack Overflow (dengan tag flutter), GitHub Issues repositori Flutter, atau forum komunitas Flutter lainnya. Saat bertanya, sertakan output lengkap dari flutter doctor -v untuk membantu orang lain memahami masalah Anda.

Bab 1.4: Membuat Proyek Flutter Pertama

Setelah memahami persyaratan sistem dan cara mengatasi masalah umum, saatnya untuk masuk ke bagian praktik, yaitu membuat dan menjalankan proyek Flutter pertama Anda. Namun, sebelum itu, penting untuk memahami struktur direktori yang akan Anda temui.

Struktur Proyek Flutter

Saat Anda membuat proyek baru menggunakan perintah flutter create, Flutter akan menghasilkan serangkaian file dan folder dengan struktur yang terorganisir. Memahami struktur ini adalah kunci untuk menavigasi dan mengembangkan aplikasi Anda secara efisien.

Berikut adalah penjelasan mengenai direktori dan file utama dalam sebuah proyek Flutter:

  • lib/ Ini adalah direktori terpenting dalam proyek Anda. Sebagian besar kode Dart yang akan Anda tulis akan berada di sini.

    • main.dart: Ini adalah titik masuk (entry point) utama dari aplikasi Anda. Eksekusi kode dimulai dari file ini.
  • android/ dan ios/ Direktori ini berisi proyek native untuk masing-masing platform.

    • android/: Berisi semua file yang diperlukan untuk membangun aplikasi Android, termasuk file Gradle dan AndroidManifest.xml.
    • ios/: Berisi semua file yang diperlukan untuk membangun aplikasi iOS, termasuk konfigurasi Xcode dan Info.plist. Anda biasanya tidak perlu sering mengubah file di dalam direktori ini, kecuali saat melakukan konfigurasi spesifik untuk platform tertentu (misalnya, menambahkan izin atau mengintegrasikan pustaka native).
  • web/, windows/, macos/, linux/ Sama seperti android/ dan ios/, direktori ini berisi file konfigurasi untuk membangun aplikasi Anda di platform web, desktop Windows, macOS, dan Linux.

  • test/ Direktori ini adalah tempat untuk menyimpan semua file pengujian (testing) Anda. Flutter mendukung berbagai jenis pengujian, seperti unit test, widget test, dan integration test.

  • pubspec.yaml Ini adalah file konfigurasi utama untuk proyek Flutter Anda. Di sinilah Anda akan:

    • Mendeklarasikan dependensi atau package eksternal yang digunakan dalam proyek.
    • Menambahkan aset seperti gambar, font, atau file JSON.
    • Mengatur informasi metadata proyek seperti nama, deskripsi, dan versi aplikasi.
  • build/ Direktori ini dibuat secara otomatis saat Anda membangun atau menjalankan aplikasi. Isinya adalah hasil kompilasi dari kode Anda untuk platform target. Anda tidak perlu mengubah isi direktori ini secara manual.

  • .gitignore File ini berisi daftar file dan direktori yang akan diabaikan oleh sistem kontrol versi Git, seperti direktori build/, .dart_tool/, dan file konfigurasi lokal lainnya.

Dengan memahami struktur dasar ini, Anda akan lebih mudah mengelola file dan mengembangkan aplikasi yang lebih kompleks di kemudian hari.

Membuat dan Menjalankan Proyek

Sekarang, mari kita praktikkan cara membuat, menjalankan, dan memodifikasi proyek Flutter pertama Anda.

Langkah 1: Membuat Proyek Baru

  1. Buka terminal atau Command Prompt.

  2. Navigasikan ke direktori tempat Anda ingin menyimpan proyek (misalnya, ~/development atau C:\Users\Anda\Documents).

  3. Jalankan perintah flutter create diikuti dengan nama proyek Anda. Nama proyek harus menggunakan format snake_case (huruf kecil dan garis bawah sebagai pemisah).

    flutter create proyek_pertama_saya
    

    Perintah ini akan membuat sebuah direktori baru bernama proyek_pertama_saya yang berisi aplikasi Flutter demo sederhana.

  4. Masuk ke direktori proyek yang baru dibuat:

    cd proyek_pertama_saya
    

Langkah 2: Menjalankan Aplikasi

Sebelum menjalankan aplikasi, pastikan Anda memiliki perangkat target yang aktif, baik itu emulator, simulator, atau perangkat fisik yang terhubung.

  1. Pilih Perangkat Target:

    • Emulator/Simulator: Buka Android Studio atau Xcode untuk meluncurkan emulator Android atau simulator iOS.
    • Perangkat Fisik: Hubungkan perangkat Android atau iOS Anda ke komputer melalui USB dan pastikan mode developer serta USB debugging telah diaktifkan.
    • Web: Anda juga dapat menjalankan aplikasi di browser Chrome.
  2. Jalankan Aplikasi: Di dalam direktori proyek, jalankan perintah berikut:

    flutter run
    

    Flutter akan membangun aplikasi dan menginstalnya di perangkat target yang aktif. Proses ini mungkin memakan waktu beberapa saat saat pertama kali dijalankan. Setelah selesai, aplikasi demo akan terbuka di perangkat Anda.

Langkah 3: Mengenal Hot Reload

Salah satu fitur paling kuat dari Flutter adalah Hot Reload. Fitur ini memungkinkan Anda untuk melihat perubahan pada kode secara instan tanpa harus memulai ulang aplikasi.

Mari kita coba:

  1. Buka file lib/main.dart di editor kode Anda (misalnya, Visual Studio Code atau Android Studio).
  2. Cari baris kode yang berisi teks 'Flutter Demo Home Page'.
  3. Ubah teks tersebut menjadi sesuatu yang lain, misalnya 'Proyek Pertamaku'.
    // Ganti ini:
    title: 'Flutter Demo Home Page',
    
    // Menjadi ini:
    title: 'Proyek Pertamaku',
    
  4. Simpan perubahan pada file tersebut (Ctrl + S atau Cmd + S).
  5. Perhatikan aplikasi Anda di perangkat. Judul aplikasi akan langsung berubah tanpa aplikasi dimulai ulang.

Itulah keajaiban Hot Reload! Fitur ini sangat mempercepat proses pengembangan, terutama saat Anda sedang membangun antarmuka pengguna (UI) dan ingin mencoba berbagai perubahan dengan cepat.

Kapan menggunakan Hot Reload vs. Hot Restart?

  • Hot Reload (tekan r di terminal): Digunakan untuk perubahan UI dan logika minor. State aplikasi tetap terjaga.
  • Hot Restart (tekan Shift + R di terminal): Digunakan untuk perubahan yang lebih besar, seperti mengubah main() atau initState(). State aplikasi akan di-reset.

Bab 2: Dasar Pemrograman Dart

Selamat datang di bab kedua! Pada bagian ini, kita akan menyelami dasar-dasar bahasa pemrograman Dart, yang merupakan fondasi dari Flutter. Dart adalah bahasa yang modern, fleksibel, dan dioptimalkan untuk membangun aplikasi di berbagai platform dari satu basis kode.

Di bab ini, kita akan mempelajari:

  • Fitur-fitur modern Dart: Termasuk type inference, null safety, dan bagaimana mengelola data yang bisa berubah (mutable) dan tidak bisa berubah (immutable).
  • Tipe data dasar: Seperti String untuk teks, num untuk angka, dan bool untuk nilai benar/salah.
  • Struktur data: Seperti List untuk kumpulan data dan Map untuk pasangan kunci-nilai.
  • Penerapan praktis: Melalui studi kasus pembuatan program pengelolaan kontak sederhana.

Mari kita mulai dengan menjelajahi fitur-fitur modern yang membuat Dart menjadi bahasa yang kuat dan efisien.

Bab 2: Dasar Pemrograman Dart

Selamat datang di bab kedua! Pada bagian ini, kita akan menyelami dasar-dasar bahasa pemrograman Dart, yang merupakan fondasi dari Flutter. Dart adalah bahasa yang modern, fleksibel, dan dioptimalkan untuk membangun aplikasi di berbagai platform dari satu basis kode.

Dart sebagai Bahasa Pemrograman Modern

Dart dikembangkan oleh Google dengan tujuan untuk menjadi bahasa yang mudah dipelajari, efisien, dan mampu menghasilkan aplikasi berkinerja tinggi. Beberapa fitur modern yang membuatnya menonjol adalah:

1. Type Inference (Penyimpulan Tipe)

Dalam banyak bahasa pemrograman, Anda harus secara eksplisit mendeklarasikan tipe data dari sebuah variabel (misalnya, int age = 30;). Dart juga mendukung ini, tetapi ia juga pintar dalam menyimpulkan tipe data secara otomatis. Ini membuat kode lebih ringkas dan mudah dibaca.

Menggunakan var

Kata kunci var digunakan untuk mendeklarasikan variabel yang nilainya dapat berubah (mutable). Dart akan secara otomatis menentukan tipe data saat pertama kali variabel diinisialisasi.

Contoh:

// Dart secara otomatis mengenali 'name' sebagai String
var name = 'John Doe';

// Anda tidak bisa mengubah tipe datanya nanti
// name = 123; // Ini akan menghasilkan error

Menggunakan final

Kata kunci final juga menggunakan type inference. final digunakan untuk mendeklarasikan variabel yang nilainya tidak akan pernah berubah setelah diinisialisasi (immutable).

Contoh:

final String greeting = 'Hello';
final year = 2023; // Tipe data int diinferensikan secara otomatis

// greeting = 'Hi'; // Ini akan menghasilkan error karena variabel final tidak bisa diubah

2. Mutability Control (Kontrol Perubahan Nilai)

Dart memberikan kontrol yang jelas atas apakah sebuah variabel dapat diubah nilainya atau tidak.

  • final: Seperti yang telah disebutkan, final digunakan untuk variabel yang nilainya hanya akan diatur sekali. Ini sangat berguna untuk nilai yang Anda tahu tidak akan berubah, seperti konstanta atau data yang diterima dari sumber eksternal. Nilai final ditentukan saat runtime.

  • const: Digunakan untuk nilai yang sudah diketahui pada saat kompilasi (compile-time constant). Ini lebih ketat daripada final. const tidak hanya membuat variabel itu sendiri tidak dapat diubah, tetapi juga memastikan bahwa nilainya adalah konstanta waktu kompilasi. Menggunakan const dapat meningkatkan performa aplikasi Anda.

Contoh:

// Benar: Nilai '10' sudah diketahui saat kompilasi
const int maxUsers = 10;

// Salah: DateTime.now() hanya bisa diketahui saat runtime
// const DateTime compileTimeError = DateTime.now();

3. Keamanan Null (Null Safety)

Salah satu fitur paling kuat di Dart adalah null safety. Fitur ini membantu Anda menghindari kesalahan yang disebabkan oleh nilai null (sering disebut "the billion-dollar mistake" dalam dunia pemrograman).

Secara default, semua tipe data di Dart tidak bisa bernilai null. Jika Anda ingin sebuah variabel bisa menampung nilai null, Anda harus secara eksplisit menyatakannya dengan menambahkan tanda tanya (?) setelah tipe data.

  • Tipe Non-Nullable (Default):

    String name = "Alice";
    // name = null; // Ini akan menghasilkan error
    
  • Tipe Nullable:

    String? middleName; // Bisa bernilai null
    middleName = 'Grace';
    middleName = null; // Ini valid
    
  • Operator ! (Bang Operator): Jika Anda 100% yakin bahwa sebuah variabel yang bisa bernilai null sebenarnya tidak null pada titik tertentu, Anda bisa menggunakan operator ! untuk memberitahu Dart agar memperlakukannya sebagai non-nullable. Gunakan dengan hati-hati, karena jika ternyata nilainya null, aplikasi Anda akan mengalami runtime error.

    String? maybeName;
    // ... (logika yang memastikan maybeName tidak null)
    String name = maybeName!; // Memberi tahu Dart bahwa kita yakin maybeName tidak null
    

Dengan fitur-fitur ini, Dart membantu Anda menulis kode yang lebih aman, lebih bersih, dan lebih mudah dipelihara.

Tipe Data Fundamental di Dart

Dart adalah bahasa yang diketik secara statis, yang berarti setiap variabel memiliki tipe yang ditentukan pada saat kompilasi. Berikut adalah beberapa tipe data dasar yang paling sering digunakan:

1. Numbers (num, int, double)

Dart memiliki dua jenis tipe data untuk angka:

  • int: Untuk bilangan bulat (integer).

    int quantity = 5;
    int hexValue = 0xFF; // Juga mendukung format heksadesimal
    
  • double: Untuk bilangan desimal (floating-point).

    double price = 19.99;
    double exponent = 1.2e3; // 1.2 x 10^3
    
  • num: Tipe ini bisa menampung int maupun double.

    num myNumber = 10; // Bisa berupa integer
    myNumber = 15.5;   // atau bisa juga berupa double
    

2. Strings (String)

Tipe String digunakan untuk menyimpan teks. Anda bisa menggunakan tanda kutip tunggal (') atau ganda (").

String singleQuote = 'Ini adalah string.';
String doubleQuote = "Ini juga string.";

Untuk menyisipkan nilai variabel ke dalam string, gunakan $ (interpolasi string):

String name = 'Budi';
String greeting = 'Halo, $name!'; // Menghasilkan "Halo, Budi!"

3. Booleans (bool)

Tipe bool hanya memiliki dua nilai: true dan false.

bool isLoading = true;
bool isFinished = false;

4. Lists (atau Arrays)

List adalah kumpulan objek yang terurut. Ini mirip dengan array di bahasa lain.

// Membuat list string
List<String> fruits = ['Apel', 'Pisang', 'Jeruk'];

// Mengakses elemen berdasarkan indeks (dimulai dari 0)
String firstFruit = fruits[0]; // 'Apel'

// Menambah elemen baru
fruits.add('Mangga');

// Menghapus elemen
fruits.remove('Pisang');

5. Maps (atau Dictionaries)

Map adalah kumpulan pasangan kunci-nilai (key-value pairs). Setiap kunci harus unik.

// Membuat map dengan kunci String dan nilai dinamis
Map<String, dynamic> person = {
  'name': 'Andi',
  'age': 25,
  'isStudent': true
};

// Mengakses nilai berdasarkan kunci
String name = person['name']; // 'Andi'

// Menambahkan atau mengubah nilai
person['city'] = 'Jakarta';
person['age'] = 26;

Tipe-tipe data ini adalah fondasi untuk membangun struktur data yang lebih kompleks dalam aplikasi Flutter Anda.

Studi Kasus: Membuat Program Pengelolaan Data Kontak Sederhana

Mari kita terapkan konsep-konsep yang telah kita pelajari dalam sebuah studi kasus kecil. Kita akan membuat program sederhana untuk mengelola data kontak.

Langkah 1: Mendefinisikan Data Kontak

Pertama, kita akan mendefinisikan data untuk satu kontak. Kita akan menggunakan final untuk nilai yang tidak akan berubah dan var untuk nilai yang mungkin akan kita ubah nanti.

void main() {
  // Menggunakan 'final' karena nama dan tanggal lahir tidak akan berubah
  final String name = 'Budi Santoso';
  final int birthYear = 1995;

  // Menggunakan 'var' karena nomor telepon bisa saja diubah
  var phoneNumber = '081234567890';

  // Menggunakan List untuk menyimpan beberapa hobi
  List<String> hobbies = ['Membaca', 'Berenang', 'Menulis'];

  // Menghitung umur
  var currentYear = DateTime.now().year;
  var age = currentYear - birthYear;

  print('Nama: $name');
  print('Umur: $age tahun');
  print('Nomor Telepon: $phoneNumber');
  print('Hobi: $hobbies');
}

Langkah 2: Menangani Data yang Mungkin Kosong (Null Safety)

Tidak semua kontak memiliki alamat email. Kita bisa menggunakan tipe data nullable (String?) untuk menangani ini.

void main() {
  // ... (kode dari Langkah 1)

  // Alamat email bisa jadi null
  String? email;

  // Fungsi untuk menampilkan email jika ada
  void printEmail(String? emailAddress) {
    if (emailAddress != null) {
      print('Email: $emailAddress');
    } else {
      print('Email: (tidak ada)');
    }
  }

  printEmail(email); // Output: Email: (tidak ada)

  // Sekarang kita tambahkan email
  email = 'budi.santoso@example.com';
  printEmail(email); // Output: Email: budi.santoso@example.com
}

Langkah 3: Mengelompokkan Data dengan Map

Menggunakan variabel terpisah bisa menjadi tidak praktis. Mari kita kelompokkan semua informasi kontak ke dalam sebuah Map.

void main() {
  // ... (kode dari Langkah 1 & 2)

  // Membuat Map untuk menyimpan data kontak
  final Map<String, dynamic> contact = {
    'name': 'Budi Santoso',
    'age': 28,
    'phone': '081234567890',
    'hobbies': ['Membaca', 'Berenang', 'Menulis'],
    'email': 'budi.santoso@example.com',
    'is_active': true
  };

  // Mengakses data dari Map
  print('Nama dari Map: ${contact['name']}');
  print('Hobi pertama: ${contact['hobbies'][0]}');
}

Langkah 4: Menampilkan Data Kontak dengan Rapi

Sekarang, mari kita buat fungsi yang dapat menampilkan detail kontak dari Map yang sudah kita buat.

void displayContact(Map<String, dynamic> contact) {
  print('--- Detail Kontak ---');
  print('Nama: ${contact['name']}');
  print('Umur: ${contact['age']} tahun');
  print('Telepon: ${contact['phone']}');
  
  // Menampilkan hobi
  List<String> hobbies = contact['hobbies'];
  print('Hobi:');
  for (var hobby in hobbies) {
    print('- $hobby');
  }

  // Menampilkan email dengan pengecekan null
  String? email = contact['email'];
  if (email != null) {
    print('Email: $email');
  } else {
    print('Email: Tidak tersedia');
  }
  
  print('Status Aktif: ${contact['is_active'] ? 'Ya' : 'Tidak'}');
  print('---------------------');
}

void main() {
  final Map<String, dynamic> contact = {
    'name': 'Budi Santoso',
    'age': 28,
    'phone': '081234567890',
    'hobbies': ['Membaca', 'Berenang', 'Menulis'],
    'email': 'budi.santoso@example.com',
    'is_active': true
  };

  displayContact(contact);
}

Dengan studi kasus ini, kita telah menerapkan konsep-konsep dasar Dart seperti variabel, tipe data, null safety, dan struktur data seperti List dan Map dalam konteks yang praktis.

Bab 3: Widget dan Layout Flutter

Selamat datang di Bab 3! Pada bagian ini, kita akan menyelami inti dari pengembangan antarmuka (UI) dengan Flutter, yaitu Widget.

Flutter memiliki filosofi unik di mana "segalanya adalah widget". Mulai dari elemen struktural seperti tombol dan teks, hingga elemen tata letak seperti baris dan kolom, bahkan sampai padding dan alignment, semuanya adalah widget.

Di bab ini, kita akan belajar:

  • Konsep dasar widget dan mengapa ia begitu penting di Flutter.
  • Berbagai jenis widget yang tersedia.
  • Cara menyusun widget untuk membangun layout yang kompleks dan responsif.

Mari kita mulai perjalanan kita untuk menjadi ahli dalam membangun UI yang indah dan fungsional dengan Flutter!

Pengenalan Flutter dan Widget

Apa itu Widget?

Dalam Flutter, Widget adalah blok bangunan dasar untuk membuat antarmuka pengguna (UI). Anggap saja widget seperti potongan-potongan LEGO yang dapat Anda susun untuk membangun aplikasi Anda. Setiap widget adalah representasi dari bagian tertentu dari UI, seperti teks, tombol, gambar, atau bahkan tata letak yang tidak terlihat.

Widget di Flutter bersifat immutable (tidak dapat diubah). Ini berarti bahwa setiap kali framework perlu memperbarui tampilan, ia akan membuat instance widget baru, bukan memodifikasi yang lama. Proses ini sangat efisien dan membantu Flutter mencapai performa rendering yang tinggi.

Filosofi "Everything is a Widget"

Salah satu konsep paling kuat di Flutter adalah "segalanya adalah widget". Ini berarti bahwa hampir semua yang Anda buat di Flutter, dari hal paling sederhana hingga paling kompleks, adalah sebuah widget.

  • Sebuah Text adalah widget.
  • Sebuah Image adalah widget.
  • Padding (untuk memberi ruang) adalah widget.
  • Center (untuk memposisikan di tengah) adalah widget.
  • Row dan Column (untuk tata letak) adalah widget.
  • Bahkan aplikasi Anda secara keseluruhan (MaterialApp atau CupertinoApp) adalah sebuah widget.

Filosofi ini memberikan konsistensi dan kemudahan dalam menyusun UI. Anda tidak perlu belajar konsep yang berbeda untuk layout, styling, atau logika bisnis. Anda hanya perlu berpikir tentang bagaimana cara menyusun widget.

Hierarki Widget (Widget Tree)

Aplikasi Flutter dibangun dengan menyusun widget dalam sebuah hierarki yang disebut Widget Tree. Ada widget induk (parent) dan widget anak (child/children). Widget induk dapat berisi satu atau lebih widget anak, membentuk struktur seperti pohon.

Misalnya, untuk membuat sebuah halaman sederhana, Anda mungkin memiliki struktur seperti ini:

Scaffold
└── Center
    └── Column
        ├── Text
        ├── Button
        └── Image
  • Scaffold adalah widget dasar untuk layout halaman Material Design.
  • Center adalah widget yang memposisikan anaknya di tengah.
  • Column adalah widget yang menyusun anak-anaknya secara vertikal.
  • Text, Button, dan Image adalah widget-widget yang menampilkan konten.

Flutter akan "berjalan" melalui pohon widget ini untuk merender UI ke layar. Memahami bagaimana widget tree bekerja adalah kunci untuk membangun layout yang kompleks dan efisien di Flutter.

Jenis-jenis Widget

Secara garis besar, widget di Flutter dapat dikategorikan menjadi beberapa jenis berdasarkan fungsi dan karakteristiknya. Dua kategorisasi yang paling penting untuk dipahami adalah Stateless vs Stateful Widget dan UI vs Layout Widget.

1. StatelessWidget vs StatefulWidget

Ini adalah klasifikasi mendasar yang berkaitan dengan bagaimana sebuah widget menangani state atau data dinamis.

StatelessWidget

StatelessWidget adalah widget yang tidak memiliki state internal. Artinya, setelah widget ini dibuat, tampilannya tidak dapat berubah. Properti atau konfigurasinya diterima dari widget induknya dan bersifat immutable (tidak bisa diubah).

Kapan menggunakan StatelessWidget? Gunakan StatelessWidget ketika bagian dari UI yang Anda bangun tidak bergantung pada data yang bisa berubah di dalam widget itu sendiri. Contohnya:

  • Ikon (Icon)
  • Teks statis (Text)
  • Tombol tanpa logika internal yang kompleks (ElevatedButton)

Contoh Kode:

class MyStaticCard extends StatelessWidget {
  final String title;
  final String description;

  const MyStaticCard({Key? key, required this.title, required this.description}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: [
          Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
          Text(description),
        ],
      ),
    );
  }
}

StatefulWidget

StatefulWidget adalah widget yang memiliki state internal yang dapat berubah selama lifecycle (siklus hidup) widget tersebut. Ketika state berubah, widget ini akan memberi tahu Flutter untuk membangun ulang (rebuild) tampilannya dengan data yang baru.

StatefulWidget sebenarnya terdiri dari dua kelas:

  1. StatefulWidget: Kelas yang bersifat immutable dan menyimpan instance dari kelas State.
  2. State: Kelas di mana state (data yang bisa berubah) disimpan dan di mana method build() berada. Perubahan state harus dilakukan di dalam setState() agar UI diperbarui.

Kapan menggunakan StatefulWidget? Gunakan StatefulWidget ketika UI perlu diperbarui secara dinamis sebagai respons terhadap interaksi pengguna atau perubahan data. Contohnya:

  • Checkbox yang bisa dicentang dan tidak dicentang.
  • Slider yang nilainya bisa digeser.
  • Form input di mana pengguna mengetikkan teks.
  • Tampilan yang datanya diambil dari internet dan bisa berubah.

Contoh Kode:

class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        ElevatedButton(onPressed: _increment, child: Text('Increment')),
        SizedBox(width: 16),
        Text('Count: $_count'),
      ],
    );
  }
}

2. UI Widget vs Layout Widget

Klasifikasi ini didasarkan pada fungsi utama widget: apakah untuk menampilkan sesuatu ke pengguna atau untuk mengatur tata letak widget lain.

UI Widget (Widget Antarmuka)

Ini adalah widget yang dilihat dan berinteraksi langsung dengan pengguna. Mereka adalah "daging" dari aplikasi Anda.

  • Contoh:
    • Text: Menampilkan string teks.
    • Image: Menampilkan gambar.
    • Icon: Menampilkan ikon grafis.
    • ElevatedButton, TextButton: Tombol yang bisa ditekan.
    • TextField: Input field untuk teks.
    • Scaffold: Struktur dasar halaman Material Design.

Layout Widget (Widget Tata Letak)

Widget ini biasanya tidak terlihat secara langsung, tetapi perannya sangat krusial: mengatur posisi, ukuran, dan susunan dari widget-widget lain (anak-anaknya).

  • Contoh:
    • Container: Kotak serbaguna yang bisa di-styling dengan padding, margin, border, warna, dll.
    • Row: Menyusun widget anaknya secara horizontal.
    • Column: Menyusun widget anaknya secara vertikal.
    • Stack: Menumpuk widget anaknya dari belakang ke depan.
    • Center: Memposisikan anaknya di tengah.
    • Padding: Memberi ruang kosong di sekitar anaknya.

Dalam praktiknya, Anda akan selalu mengkombinasikan kedua jenis widget ini. Anda akan menggunakan layout widget untuk membangun struktur halaman, dan di dalamnya Anda akan menempatkan UI widget untuk menampilkan konten yang sebenarnya.

Konsep Named Parameter pada Widget

Salah satu fitur bahasa Dart yang membuat kode Flutter sangat mudah dibaca dan deklaratif adalah penggunaan Named Parameter (parameter bernama). Saat Anda membuat instance sebuah widget, Anda hampir selalu menggunakan named parameter untuk mengonfigurasinya.

Apa itu Named Parameter?

Di Dart, fungsi atau konstruktor dapat memiliki dua jenis parameter: positional dan named.

  • Positional Parameter: Parameter yang nilainya ditentukan oleh posisinya dalam pemanggilan fungsi. Ini adalah jenis parameter standar di banyak bahasa pemrograman.
  • Named Parameter: Parameter yang diidentifikasi oleh namanya, bukan posisinya. Mereka bersifat opsional secara default dan ditandai dengan kurung kurawal {} dalam definisi fungsi/konstruktor.

Contoh Sederhana di Dart:

// Definisi fungsi dengan named parameter
void printUserDetails({String name, int age}) {
  print('Name: $name, Age: $age');
}

// Pemanggilan fungsi
printUserDetails(name: 'Alice', age: 30);
printUserDetails(age: 25, name: 'Bob'); // Urutan tidak penting

Named Parameter pada Widget Flutter

Konstruktor widget di Flutter secara ekstensif menggunakan named parameter. Ini adalah pilihan desain yang disengaja karena beberapa alasan kuat:

  1. Keterbacaan (Readability): Dengan puluhan properti yang mungkin dimiliki sebuah widget, named parameter membuat kode menjadi jauh lebih jelas. Anda tahu persis properti apa yang sedang Anda atur tanpa harus menghafal urutan parameter.

    Kurang Jelas (Tanpa Named Parameter):

    // Ini BUKAN kode Flutter, hanya ilustrasi
    // new Container(300.0, 200.0, Colors.blue, new Text("Hello"), 16.0);
    

    Sangat Jelas (Dengan Named Parameter):

    Container(
      width: 300.0,
      height: 200.0,
      color: Colors.blue,
      padding: EdgeInsets.all(16.0),
      child: Text("Hello"),
    );
    
  2. Fleksibilitas dan Opsionalitas: Sebagian besar properti widget bersifat opsional. Anda hanya perlu menentukan properti yang ingin Anda ubah dari nilai defaultnya. Named parameter sangat cocok untuk skenario ini.

  3. Kemudahan Penggunaan (Discoverability): Saat menggunakan IDE seperti VS Code atau Android Studio, IDE dapat secara otomatis menampilkan daftar named parameter yang tersedia untuk sebuah widget, membuatnya lebih mudah untuk "menemukan" properti apa saja yang bisa Anda konfigurasi.

Contoh Penggunaan pada Widget Umum

Mari kita lihat bagaimana named parameter digunakan pada beberapa widget yang sering kita temui.

Text Widget

Widget Text memiliki satu parameter positional wajib (yaitu data teks itu sendiri) dan banyak named parameter opsional untuk styling.

Text(
  'Ini adalah teks utama', // Positional parameter
  style: TextStyle(         // Named parameter
    color: Colors.red,
    fontSize: 24.0,
    fontWeight: FontWeight.bold,
  ),
  textAlign: TextAlign.center, // Named parameter
)

Container Widget

Widget Container adalah contoh utama dari kekuatan named parameter. Hampir semua konfigurasinya dilakukan melalui parameter ini.

Container(
  // Properti untuk ukuran dan batasan
  width: 150,
  height: 100,

  // Properti untuk dekorasi (tidak bisa dipakai bersamaan dengan `color`)
  decoration: BoxDecoration(
    color: Colors.amber,
    borderRadius: BorderRadius.circular(12),
  ),

  // Properti untuk tata letak internal
  padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
  alignment: Alignment.center,

  // Widget anak
  child: Text('Hello World'),
)

Dengan menggunakan named parameter, Flutter memungkinkan kita untuk membuat UI yang kompleks dengan cara yang sangat deklaratif dan mudah dipelihara. Anda "mendeskripsikan" seperti apa tampilan UI yang Anda inginkan, dan Flutter yang akan mengurus sisanya.

Layouting di Flutter

Membangun layout adalah salah satu tugas fundamental dalam pengembangan UI. Flutter menyediakan serangkaian widget layout yang sangat kaya dan fleksibel. Di bagian ini, kita akan membahas widget-widget layout yang paling umum digunakan, yang terbagi menjadi dua kategori utama: single-child dan multi-child layout widgets.

1. Single-Child Layout Widgets

Widget ini hanya dapat memiliki satu widget anak (child). Fungsinya adalah untuk memberikan styling, batasan, atau alignment tertentu pada satu widget tersebut.

Container

Container adalah widget layout yang paling serbaguna. Anggap saja ini seperti sebuah kotak kosong yang bisa Anda hias sesuka hati.

  • Properti Utama:

    • child: Widget yang akan berada di dalam Container.
    • width, height: Ukuran container.
    • color: Warna latar belakang. Penting: Jangan gunakan color jika Anda sudah menggunakan decoration.
    • decoration: Untuk styling yang lebih kompleks seperti gradien, border, atau bayangan (BoxDecoration).
    • padding: Jarak dari batas Container ke child-nya.
    • margin: Jarak dari batas Container ke widget lain di luarnya.
    • alignment: Posisi child di dalam Container.
  • Contoh:

    Container(
      width: 200,
      height: 100,
      padding: EdgeInsets.all(10),
      margin: EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.blue,
        borderRadius: BorderRadius.circular(8),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.2),
            spreadRadius: 2,
            blurRadius: 5,
            offset: Offset(0, 3),
          ),
        ],
      ),
      child: Text('Styled Box', style: TextStyle(color: Colors.white)),
    )
    

Center

Sesuai namanya, Center adalah widget sederhana yang fungsinya hanya untuk memposisikan child-nya di tengah-tengah ruang yang tersedia.

Center(
  child: Text('Hello, Center!'),
)

Padding

Widget ini lebih spesifik daripada padding di Container. Tujuannya hanya satu: memberikan ruang kosong (padding) di sekeliling child-nya.

Padding(
  padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
  child: ElevatedButton(
    onPressed: () {},
    child: Text('Padded Button'),
  ),
)

SizedBox

SizedBox adalah kotak dengan ukuran yang spesifik. Widget ini sangat berguna untuk:

  1. Memberi ukuran pasti pada sebuah widget anak.
  2. Menciptakan ruang kosong (spasi) di antara widget lain (misalnya di dalam Row atau Column).
Column(
  children: [
    Text('Item 1'),
    SizedBox(height: 20), // Spasi vertikal 20 pixel
    Text('Item 2'),
  ],
)

AspectRatio

Widget ini memaksa child-nya untuk memiliki rasio aspek tertentu (perbandingan lebar dan tinggi).

AspectRatio(
  aspectRatio: 16 / 9, // Rasio video widescreen
  child: Container(color: Colors.green),
)

2. Multi-Child Layout Widgets

Widget ini dapat memiliki banyak widget anak dalam sebuah list (children). Fungsinya adalah untuk menyusun widget-widget tersebut dalam pola tertentu.

Row dan Column

Ini adalah dua widget layout multi-child yang paling fundamental.

  • Row: Menyusun children-nya secara horizontal.

  • Column: Menyusun children-nya secara vertikal.

  • Properti Utama:

    • children: List widget yang akan disusun.
    • mainAxisAlignment: Mengatur alignment children di sumbu utama (horizontal untuk Row, vertical untuk Column).
      • MainAxisAlignment.start (default)
      • MainAxisAlignment.center
      • MainAxisAlignment.end
      • MainAxisAlignment.spaceBetween
      • MainAxisAlignment.spaceAround
      • MainAxisAlignment.spaceEvenly
    • crossAxisAlignment: Mengatur alignment children di sumbu silang (vertical untuk Row, horizontal untuk Column).
      • CrossAxisAlignment.start
      • CrossAxisAlignment.center (default)
      • CrossAxisAlignment.end
      • CrossAxisAlignment.stretch
  • Contoh Row:

    Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        Icon(Icons.star),
        Icon(Icons.star),
        Icon(Icons.star),
      ],
    )
    

Stack

Stack memungkinkan Anda untuk menumpuk widget di atas satu sama lain, seperti tumpukan kartu. Widget pertama dalam list children akan berada di paling bawah, dan widget terakhir akan berada di paling atas.

  • Contoh:
    Stack(
      alignment: Alignment.center,
      children: [
        Container(width: 200, height: 200, color: Colors.red),
        Container(width: 150, height: 150, color: Colors.green),
        Text('On Top'),
      ],
    )
    

ListView

ListView adalah widget yang menyusun children-nya dalam daftar yang dapat di-scroll. Ini sangat efisien untuk menampilkan daftar item yang panjang.

ListView(
  children: [
    ListTile(leading: Icon(Icons.map), title: Text('Map')),
    ListTile(leading: Icon(Icons.photo_album), title: Text('Album')),
    ListTile(leading: Icon(Icons.phone), title: Text('Phone')),
  ],
)

GridView

GridView menyusun children-nya dalam format grid (kisi-kisi) 2D yang dapat di-scroll.

GridView.count(
  crossAxisCount: 2, // Jumlah kolom
  children: List.generate(10, (index) {
    return Center(
      child: Text(
        'Item $index',
        style: Theme.of(context).textTheme.headline5,
      ),
    );
  }),
)

Menguasai widget-widget layout ini adalah langkah penting untuk dapat membangun hampir semua jenis tampilan yang bisa Anda bayangkan di Flutter.

Studi Kasus dan Praktek

Teori tanpa praktek akan terasa kurang lengkap. Di bagian ini, kita akan mengaplikasikan semua konsep widget dan layout yang telah kita pelajari untuk membangun beberapa komponen UI sederhana.

Praktek 1: Membuat Kartu Nama Digital

Mari kita buat sebuah kartu nama digital sederhana yang menampilkan foto, nama, jabatan, dan informasi kontak.

Tujuan:

  • Mengkombinasikan Container, Column, Row, CircleAvatar, Text, dan Icon.
  • Menggunakan SizedBox untuk memberikan spasi.
  • Menerapkan mainAxisAlignment dan crossAxisAlignment.

Kode Lengkap:

import 'package:flutter/material.dart';

class DigitalBusinessCard extends StatelessWidget {
  const DigitalBusinessCard({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.teal,
      body: SafeArea(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              CircleAvatar(
                radius: 50.0,
                backgroundImage: NetworkImage('https://i.pravatar.cc/150?img=3'),
              ),
              Text(
                'Jane Doe',
                style: TextStyle(
                  fontFamily: 'Pacifico',
                  fontSize: 40.0,
                  color: Colors.white,
                  fontWeight: FontWeight.bold,
                ),
              ),
              Text(
                'FLUTTER DEVELOPER',
                style: TextStyle(
                  fontFamily: 'Source Sans Pro',
                  color: Colors.teal.shade100,
                  fontSize: 20.0,
                  letterSpacing: 2.5,
                  fontWeight: FontWeight.bold,
                ),
              ),
              SizedBox(
                height: 20.0,
                width: 150.0,
                child: Divider(
                  color: Colors.teal.shade100,
                ),
              ),
              Card(
                margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 25.0),
                child: ListTile(
                  leading: Icon(
                    Icons.phone,
                    color: Colors.teal,
                  ),
                  title: Text(
                    '+62 123 4567 890',
                    style: TextStyle(
                      color: Colors.teal.shade900,
                      fontFamily: 'Source Sans Pro',
                      fontSize: 20.0,
                    ),
                  ),
                ),
              ),
              Card(
                margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 25.0),
                child: ListTile(
                  leading: Icon(
                    Icons.email,
                    color: Colors.teal,
                  ),
                  title: Text(
                    'jane.doe@example.com',
                    style: TextStyle(
                      fontSize: 20.0,
                      color: Colors.teal.shade900,
                      fontFamily: 'Source Sans Pro',
                    ),
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

Catatan: Untuk font Pacifico dan Source Sans Pro, Anda perlu menambahkannya ke pubspec.yaml dan folder assets terlebih dahulu.

Praktek 2: Membuat Layout Galeri Gambar Sederhana

Sekarang, kita akan membuat sebuah galeri gambar sederhana menggunakan GridView.

Tujuan:

  • Menggunakan GridView.count untuk membuat layout grid.
  • Menempatkan Image di dalam Container yang diberi BoxShadow.

Kode Lengkap:

import 'package:flutter/material.dart';

class SimpleImageGallery extends StatelessWidget {
  const SimpleImageGallery({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Image Gallery'),
      ),
      body: GridView.count(
        crossAxisCount: 2, // 2 kolom
        mainAxisSpacing: 8, // Spasi vertikal
        crossAxisSpacing: 8, // Spasi horizontal
        padding: EdgeInsets.all(8),
        children: List.generate(10, (index) {
          return Container(
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(12),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.1),
                  blurRadius: 5,
                  spreadRadius: 1,
                )
              ],
            ),
            child: ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: Image.network(
                'https://picsum.photos/200/200?random=$index',
                fit: BoxFit.cover,
              ),
            ),
          );
        }),
      ),
    );
  }
}

Praktek 3: Layout Responsif Sederhana

Terakhir, mari kita coba membuat layout yang sedikit beradaptasi dengan ukuran layar. Kita akan membuat sebuah Container yang warnanya berubah jika layarnya lebih lebar dari 600 pixel.

Tujuan:

  • Menggunakan LayoutBuilder untuk mendapatkan informasi batasan (constraints) dari parent widget.
  • Membuat kondisi sederhana berdasarkan maxWidth.

Kode Lengkap:

import 'package:flutter/material.dart';

class ResponsiveLayout extends StatelessWidget {
  const ResponsiveLayout({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Responsive Layout'),
      ),
      body: Center(
        child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            // constraints.maxWidth akan berisi lebar maksimal yang tersedia
            if (constraints.maxWidth > 600) {
              // Jika layar lebar (misal: tablet, desktop)
              return Container(
                width: 400,
                height: 200,
                color: Colors.green,
                child: Center(
                  child: Text(
                    'Wide Layout (width: ${constraints.maxWidth.toStringAsFixed(0)})',
                    style: TextStyle(color: Colors.white, fontSize: 24),
                  ),
                ),
              );
            } else {
              // Jika layar sempit (misal: handphone)
              return Container(
                width: 200,
                height: 200,
                color: Colors.blue,
                child: Center(
                  child: Text(
                    'Narrow Layout (width: ${constraints.maxWidth.toStringAsFixed(0)})',
                    style: TextStyle(color: Colors.white, fontSize: 18),
                  ),
                ),
              );
            }
          },
        ),
      ),
    );
  }
}

Dengan menyelesaikan praktek-praktek ini, Anda telah mengambil langkah besar dalam memahami bagaimana menyusun dan membangun antarmuka pengguna yang nyata dengan Flutter.

Bab 4: Navigasi dalam Aplikasi Flutter

Selamat datang di Bab 4! Hampir semua aplikasi mobile terdiri dari beberapa layar. Kemampuan untuk berpindah antar layar ini, atau yang kita sebut navigasi, adalah salah satu konsep fundamental dalam pengembangan aplikasi.

Di Flutter, navigasi bekerja seperti tumpukan (stack). Anda "mendorong" (push) layar baru ke atas tumpukan untuk menampilkannya, dan "melepas" (pop) layar tersebut untuk kembali ke layar sebelumnya.

Dalam bab ini, kita akan membahas:

  • Konsep dasar di balik sistem navigasi Flutter.
  • Berbagai metode untuk berpindah antar layar, baik secara anonim maupun menggunakan rute bernama (named routes).
  • Cara mengirim data ke layar baru dan menerima data kembali.
  • Mengimplementasikan alur navigasi yang umum dalam sebuah studi kasus praktis.

Mari kita pelajari cara membuat aplikasi kita menjadi lebih interaktif dengan menambahkan alur navigasi yang mulus.

Konsep Fundamental Navigasi

Sebelum kita masuk ke kode, penting untuk memahami tiga konsep inti yang menjadi dasar sistem navigasi di Flutter: Stack of Pages (Routes), Navigator Widget, dan Route.

1. Stack of Pages (Tumpukan Halaman)

Bayangkan layar-layar dalam aplikasi Anda sebagai setumpuk kartu. Ketika Anda pindah ke layar baru, Anda meletakkan kartu baru di atas tumpukan. Ketika Anda menekan tombol "kembali", Anda mengambil kartu teratas dari tumpukan, sehingga kartu di bawahnya kembali terlihat.

Inilah cara kerja navigasi di Flutter. Tumpukan ini dikelola oleh Navigator dan setiap "kartu" atau "halaman" dalam tumpukan ini disebut Route.

  • Push: Operasi "mendorong" Route baru ke atas tumpukan. Ini akan menampilkan layar baru kepada pengguna.
  • Pop: Operasi "melepas" Route teratas dari tumpukan. Ini akan menutup layar saat ini dan kembali ke layar sebelumnya.

Layar pertama yang dibuka aplikasi Anda adalah dasar dari tumpukan tersebut.

2. Navigator Widget

Navigator adalah widget khusus yang mengelola tumpukan Route. Anda tidak sering berinteraksi langsung dengan widget Navigator itu sendiri, tetapi Anda akan sering menggunakan method statisnya, seperti Navigator.of(context) atau Navigator.push() dan Navigator.pop().

Secara default, MaterialApp atau CupertinoApp sudah secara otomatis membuatkan sebuah Navigator untuk Anda. Navigator inilah yang menjadi "otak" di balik semua perpindahan layar dalam aplikasi Anda.

Setiap Navigator mengelola tumpukan riwayatnya sendiri. Ini memungkinkan skenario navigasi yang kompleks, seperti memiliki alur navigasi terpisah di dalam sebuah tab.

3. Route

Sebuah Route adalah representasi dari sebuah layar atau halaman dalam tumpukan Navigator. Ini bukan hanya Widget itu sendiri, melainkan sebuah objek yang membungkus Widget Anda dan menangani hal-hal seperti transisi animasi saat layar muncul atau hilang.

Ada beberapa jenis Route, tetapi yang paling umum Anda gunakan adalah MaterialPageRoute.

MaterialPageRoute

MaterialPageRoute adalah Route yang mengimplementasikan transisi halaman standar sesuai dengan pedoman Material Design. Di Android, halaman baru akan meluncur dari bawah ke atas. Di iOS, halaman baru akan meluncur dari kanan ke kiri.

Untuk membuat MaterialPageRoute, Anda perlu menyediakan sebuah builder. builder ini adalah sebuah fungsi yang menerima BuildContext dan mengembalikan widget yang ingin Anda tampilkan untuk rute tersebut.

// Contoh membuat sebuah MaterialPageRoute
MaterialPageRoute(
  builder: (BuildContext context) {
    return SecondScreen(); // Widget untuk layar kedua
  },
)

Secara ringkas, alurnya adalah: Anda meminta Navigator untuk push sebuah Route (misalnya, MaterialPageRoute), dan Route tersebut akan build (membangun) widget layar Anda untuk ditampilkan kepada pengguna.

Metode Navigasi dalam Flutter

Flutter menyediakan dua pendekatan utama untuk navigasi: Navigasi Dasar (Anonymous Routes) dan Navigasi Bernama (Named Routes). Keduanya menggunakan Navigator di belakang layar, tetapi menawarkan cara kerja yang berbeda.

1. Navigasi Dasar (Anonymous Routes)

Ini adalah cara paling langsung untuk berpindah layar. Anda membuat Route (misalnya, MaterialPageRoute) secara on-the-fly (langsung di tempat) dan "mendorong"-nya ke Navigator.

Method ini digunakan untuk pindah ke layar baru. Anda perlu memberikan context dan sebuah Route.

Contoh: Misalkan kita punya dua widget layar: FirstScreen dan SecondScreen.

// Di dalam widget FirstScreen
ElevatedButton(
  child: Text('Go to Second Screen'),
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => SecondScreen()),
    );
  },
)

Ketika tombol ditekan, MaterialPageRoute baru dibuat, yang kemudian membangun SecondScreen, dan Navigator menampilkannya di atas FirstScreen.

Method ini digunakan untuk kembali ke layar sebelumnya dengan "melepas" Route saat ini dari tumpukan Navigator.

// Di dalam widget SecondScreen
ElevatedButton(
  child: Text('Go Back'),
  onPressed: () {
    Navigator.pop(context);
  },
)

Flutter secara otomatis menambahkan tombol "kembali" di AppBar yang juga akan memanggil Navigator.pop(context).

Mengirim Data ke Layar Baru

Cara paling umum untuk mengirim data adalah melalui konstruktor widget layar tujuan.

// Di FirstScreen, saat memanggil push
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailScreen(productId: '123'), // Kirim data via konstruktor
  ),
);

// Di DetailScreen, terima data di konstruktor
class DetailScreen extends StatelessWidget {
  final String productId;

  const DetailScreen({Key? key, required this.productId}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Product ID: $productId')),
      // ...
    );
  }
}

Mengembalikan Data dari Layar

Terkadang, layar kedua perlu mengembalikan data ke layar pertama (misalnya, layar pengaturan yang mengembalikan pilihan pengguna).

  1. Gunakan await saat memanggil Navigator.push().
  2. Gunakan Navigator.pop(result) untuk mengirim data kembali.
// Di FirstScreen
void _navigateToSelectionScreen() async {
  // Tunggu hasil dari SelectionScreen
  final result = await Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => SelectionScreen()),
  );

  // Tampilkan hasil yang dikembalikan
  ScaffoldMessenger.of(context)
    ..removeCurrentSnackBar()
    ..showSnackBar(SnackBar(content: Text('$result')));
}

// Di SelectionScreen
ElevatedButton(
  onPressed: () {
    // Kembalikan data 'Yep!' ke layar sebelumnya
    Navigator.pop(context, 'Yep!');
  },
  child: Text('Yep!'),
)

2. Navigasi Bernama (Named Routes)

Pendekatan ini memungkinkan Anda untuk mendefinisikan semua rute navigasi di satu tempat, lalu memanggilnya menggunakan nama string yang unik. Ini sangat direkomendasikan untuk aplikasi berukuran sedang hingga besar.

Keuntungan:

  • Kode Lebih Bersih: Tidak perlu membuat MaterialPageRoute setiap kali. Cukup panggil Navigator.pushNamed(context, '/routeName').
  • Tersentralisasi: Semua rute didefinisikan di satu tempat (MaterialApp), sehingga mudah dikelola.
  • Mengurangi Duplikasi: Menghindari kesalahan pengetikan dan duplikasi kode.

Langkah 1: Definisikan Rute

Di dalam MaterialApp, gunakan properti routes untuk mendaftarkan nama rute dan widget yang sesuai.

MaterialApp(
  title: 'Named Routes Demo',
  initialRoute: '/', // Rute awal saat aplikasi dibuka
  routes: {
    '/': (context) => HomeScreen(),
    '/details': (context) => DetailsScreen(),
    '/settings': (context) => SettingsScreen(),
  },
)

Langkah 2: Navigasi Menggunakan Nama

Gunakan Navigator.pushNamed() untuk berpindah layar.

// Dari HomeScreen ke DetailsScreen
ElevatedButton(
  child: Text('View Details'),
  onPressed: () {
    Navigator.pushNamed(context, '/details');
  },
)

Mengirim Argumen dengan pushNamed

Bagaimana jika kita perlu mengirim data seperti ID produk? Gunakan parameter arguments.

// Mengirim argumen
Navigator.pushNamed(
  context,
  '/details',
  arguments: 'product-id-456',
);

// Menerima argumen di layar tujuan
class DetailsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Ekstrak argumen
    final String productId = ModalRoute.of(context)!.settings.arguments as String;

    return Scaffold(
      appBar: AppBar(
        title: Text('Details for $productId'),
      ),
      // ...
    );
  }
}

Method Navigasi Bernama Lainnya

  • Navigator.pushReplacementNamed(): Pindah ke rute baru dan buang rute saat ini. Berguna untuk halaman login (setelah login berhasil, pengguna tidak bisa kembali ke halaman login).
  • Navigator.popAndPushNamed(): Pop rute saat ini, lalu push rute baru. Berguna untuk berpindah antar item di menu samping (drawer).

Studi Kasus: Aplikasi Multi-Layar

Saatnya mempraktikkan semua yang telah kita pelajari. Kita akan membangun sebuah aplikasi sederhana dengan tiga layar menggunakan Named Routes.

Skenario Aplikasi:

  1. HomeScreen: Menampilkan daftar produk. Setiap item produk dapat diklik untuk melihat detailnya. Ada juga tombol untuk membuka halaman Pengaturan.
  2. DetailScreen: Menampilkan detail produk yang dipilih.
  3. SettingsScreen: Halaman pengaturan sederhana.

Langkah 1: Struktur Proyek

Pertama, buat tiga file baru di dalam folder lib Anda untuk setiap layar:

  • lib/home_screen.dart
  • lib/detail_screen.dart
  • lib/settings_screen.dart

Langkah 2: Mendefinisikan Rute di main.dart

Kita akan mendaftarkan semua rute kita di file main.dart di dalam MaterialApp.

lib/main.dart

import 'package:flutter/material.dart';
import 'package:navigation_example/home_screen.dart';
import 'package:navigation_example/detail_screen.dart';
import 'package:navigation_example/settings_screen.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Named Routes Example',
      // 1. Definisikan initialRoute
      initialRoute: '/',
      // 2. Definisikan semua rute yang tersedia
      routes: {
        '/': (context) => HomeScreen(),
        // Kita akan menangani rute '/detail' secara khusus nanti
        // untuk bisa membaca argumen.
        '/settings': (context) => SettingsScreen(),
      },
      // 3. Gunakan onGenerateRoute untuk rute yang butuh logika tambahan
      onGenerateRoute: (settings) {
        if (settings.name == DetailScreen.routeName) {
          // Ekstrak argumen yang dikirim
          final args = settings.arguments as String;

          // Buat MaterialPageRoute
          return MaterialPageRoute(
            builder: (context) {
              return DetailScreen(productId: args);
            },
          );
        }
        // Jika rute tidak ditemukan, bisa tampilkan halaman error
        assert(false, 'Need to implement ${settings.name}');
        return null;
      },
    );
  }
}

Catatan: Menggunakan onGenerateRoute adalah cara yang lebih fleksibel untuk menangani rute, terutama saat Anda perlu meneruskan argumen yang kompleks.

Langkah 3: Membuat HomeScreen

Layar ini akan menampilkan daftar dan tombol navigasi.

lib/home_screen.dart

import 'package:flutter/material.dart';
import 'package:navigation_example/detail_screen.dart';

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Screen'),
        actions: [
          IconButton(
            icon: Icon(Icons.settings),
            onPressed: () {
              // Navigasi ke halaman settings
              Navigator.pushNamed(context, '/settings');
            },
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: 5,
        itemBuilder: (context, index) {
          final productId = 'Product ${index + 1}';
          return ListTile(
            title: Text(productId),
            onTap: () {
              // Navigasi ke halaman detail dengan mengirim argumen
              Navigator.pushNamed(
                context,
                DetailScreen.routeName,
                arguments: productId,
              );
            },
          );
        },
      ),
    );
  }
}

Langkah 4: Membuat DetailScreen

Layar ini akan menerima dan menampilkan productId.

lib/detail_screen.dart

import 'package:flutter/material.dart';

class DetailScreen extends StatelessWidget {
  // Definisikan nama rute sebagai konstanta statis
  static const routeName = '/detail';

  final String productId;

  const DetailScreen({Key? key, required this.productId}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Detail for $productId'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Showing details for $productId',
              style: TextStyle(fontSize: 20),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // Kembali ke layar sebelumnya
                Navigator.pop(context);
              },
              child: Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

Langkah 5: Membuat SettingsScreen

Ini adalah layar sederhana dengan tombol kembali.

lib/settings_screen.dart

import 'package:flutter/material.dart';

class SettingsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Settings'),
      ),
      body: Center(
        child: Text('This is the settings page.'),
      ),
    );
  }
}

Dengan mengikuti langkah-langkah ini, Anda telah berhasil mengimplementasikan alur navigasi multi-layar yang bersih dan terkelola dengan baik menggunakan named routes di Flutter.

Bab 5: Form Handling dan Data Persistence

Selamat datang di Bab 5! Di bab ini, kita akan mempelajari dua topik yang sangat penting dalam pengembangan aplikasi: cara mengumpulkan input dari pengguna menggunakan Form, dan cara menyimpan data secara lokal agar tidak hilang saat aplikasi ditutup, menggunakan Shared Preferences.

Mengelola input pengguna adalah inti dari banyak aplikasi. Mulai dari halaman login, formulir pendaftaran, hingga kolom pencarian, semuanya memerlukan cara yang andal untuk menangani, memvalidasi, dan memproses data yang dimasukkan pengguna.

Selain itu, kemampuan untuk mengingat informasi—seperti status login pengguna atau pengaturan tema—adalah hal yang membuat aplikasi terasa personal dan cerdas.

Dalam bab ini, kita akan membahas:

  • Konsep dasar dan widget yang digunakan untuk membangun form di Flutter.
  • Cara mengambil nilai, melakukan validasi, dan menangani event pada form.
  • Pengenalan tentang persistensi data lokal.
  • Implementasi praktis menggunakan shared_preferences untuk menyimpan data sederhana.
  • Studi kasus yang mengintegrasikan form login dengan penyimpanan lokal.

Mari kita mulai!

Konsep Form di Flutter

Form adalah komponen UI fundamental yang berfungsi untuk mengumpulkan data dari pengguna. Di Flutter, membangun form adalah proses yang terstruktur berkat serangkaian widget yang dirancang khusus untuk tujuan ini.

1. Pentingnya Form

Hampir setiap aplikasi membutuhkan interaksi dengan pengguna dalam bentuk input. Beberapa contoh umum meliputi:

  • Autentikasi: Halaman login dan pendaftaran.
  • Input Data: Menambahkan item baru, mengisi profil pengguna.
  • Pencarian: Kolom untuk mencari konten.
  • Feedback: Formulir kontak atau ulasan.

Flutter menyediakan cara yang kuat untuk mengelola semua skenario ini.

2. Widget Form

Widget Form adalah sebuah kontainer yang tidak terlihat secara visual. Fungsinya adalah untuk mengelompokkan beberapa field input (seperti TextFormField) menjadi satu kesatuan logis. Dengan menggunakan widget Form, Anda dapat:

  • Memvalidasi semua field di dalamnya sekaligus.
  • Menyimpan nilai dari semua field di dalamnya sekaligus.
  • Mereset semua field di dalamnya ke nilai awal.
Form(
  key: _formKey, // Kunci untuk mengidentifikasi dan mengontrol form
  child: Column(
    children: <Widget>[
      // TextFormField dan widget lainnya akan ditempatkan di sini
    ],
  ),
)

3. GlobalKey<FormState>

Untuk dapat berinteraksi dengan Form (misalnya, untuk memanggil fungsi validasi), Anda memerlukan sebuah GlobalKey. GlobalKey adalah sebuah kunci unik di seluruh aplikasi yang memungkinkan Anda mengakses objek State dari sebuah widget.

Setiap widget Form memiliki State internal yang disebut FormState. FormState inilah yang memiliki method-method seperti validate(), save(), dan reset().

Cara Penggunaan:

  1. Buat GlobalKey di dalam kelas State Anda.
    final _formKey = GlobalKey<FormState>();
    
  2. Hubungkan GlobalKey tersebut ke properti key dari widget Form Anda.
    Form(
      key: _formKey,
      // ...
    )
    
  3. Sekarang, Anda bisa menggunakan _formKey untuk mengakses FormState.
    // Contoh memanggil validasi
    if (_formKey.currentState!.validate()) {
      // Jika semua field valid...
    }
    
    Tanda seru ! digunakan karena kita yakin currentState tidak akan null setelah form dibangun.

4. Widget TextFormField

Meskipun Anda bisa menggunakan TextField biasa untuk input teks, TextFormField adalah pilihan yang tepat saat bekerja di dalam sebuah Form.

TextFormField adalah wrapper di sekitar TextField yang mengintegrasikannya dengan widget Form induk. Ini memberinya kemampuan tambahan:

  • Validasi: Memiliki properti validator untuk pemeriksaan input.
  • Penyimpanan: Memiliki properti onSaved yang dipicu saat _formKey.currentState.save() dipanggil.
  • Reset: Nilainya akan direset saat _formKey.currentState.reset() dipanggil.
TextFormField(
  decoration: InputDecoration(
    labelText: 'Enter your email',
  ),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Please enter some text';
    }
    return null; // null berarti valid
  },
  onSaved: (value) {
    // Simpan nilai ini
  },
)

Dengan memahami keempat elemen ini—Form, GlobalKey<FormState>, dan TextFormField—Anda sudah memiliki fondasi yang kuat untuk membangun dan mengelola form di aplikasi Flutter Anda.

Interaksi dengan Form

Setelah memahami konsep dasar Form dan TextFormField, langkah selanjutnya adalah mempelajari cara berinteraksi dengannya: bagaimana cara mengambil nilai yang dimasukkan pengguna, bagaimana cara memvalidasinya, dan bagaimana cara merespons event yang terjadi.

1. Pengambilan Nilai dari Form

Ada dua cara utama untuk mendapatkan data dari TextFormField.

Menggunakan TextEditingController

Ini adalah pendekatan yang paling umum, terutama jika Anda memerlukan akses ke nilai field secara real-time atau di luar proses save dari Form.

Langkah-langkah:

  1. Buat sebuah TextEditingController untuk setiap TextFormField.
    final _emailController = TextEditingController();
    
  2. Hubungkan controller tersebut ke properti controller pada TextFormField.
    TextFormField(
      controller: _emailController,
      // ...
    )
    
  3. Akses nilainya kapan pun Anda butuhkan melalui properti .text.
    print('Current email value: ${_emailController.text}');
    
  4. Penting: Jangan lupa untuk membuang (dispose) controller ketika widget tidak lagi digunakan untuk mencegah kebocoran memori.
    @override
    void dispose() {
      _emailController.dispose();
      super.dispose();
    }
    

Menggunakan onSaved

Pendekatan ini sangat cocok jika Anda hanya perlu mengumpulkan semua data dari form pada saat proses submit.

Langkah-langkah:

  1. Buat variabel untuk menampung nilai yang akan disimpan.
    String _userEmail = '';
    
  2. Implementasikan callback onSaved pada TextFormField. Callback ini akan memberikan nilai akhir dari field tersebut.
    TextFormField(
      // ...
      onSaved: (value) {
        _userEmail = value ?? '';
      },
    )
    
  3. Panggil _formKey.currentState!.save() untuk memicu semua callback onSaved di dalam Form.
    ElevatedButton(
      onPressed: () {
        if (_formKey.currentState!.validate()) {
          // Panggil save() setelah validasi berhasil
          _formKey.currentState!.save();
          print('Saved email: $_userEmail');
        }
      },
      child: Text('Submit'),
    )
    

2. Form Validation (Validasi Form)

Validasi memastikan bahwa data yang dimasukkan pengguna sesuai dengan format yang kita harapkan.

Properti validator

Setiap TextFormField memiliki properti validator, yaitu sebuah fungsi yang menerima nilai input saat ini (String?) dan harus mengembalikan:

  • null jika inputnya valid.
  • Sebuah String yang berisi pesan error jika inputnya tidak valid. Pesan error ini akan otomatis ditampilkan di bawah field.

Contoh Validasi:

TextFormField(
  decoration: InputDecoration(labelText: 'Password'),
  obscureText: true,
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Password tidak boleh kosong';
    }
    if (value.length < 6) {
      return 'Password harus lebih dari 6 karakter';
    }
    return null; // Valid
  },
)

Memicu Validasi

Validasi tidak terjadi secara otomatis. Anda harus memicunya secara manual dengan memanggil _formKey.currentState!.validate().

Method validate() akan menjalankan semua fungsi validator di dalam Form dan mengembalikan:

  • true jika semua field valid.
  • false jika setidaknya ada satu field yang tidak valid.

Ini memungkinkan Anda untuk membuat alur seperti ini:

// Di dalam onPressed sebuah tombol
if (_formKey.currentState!.validate()) {
  // Jika form valid, lanjutkan proses (misalnya, kirim data ke server)
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text('Processing Data')),
  );
} else {
  // Jika form tidak valid, error akan otomatis ditampilkan
}

3. Form Events

Selain onSaved, ada beberapa event lain yang berguna.

  • onChanged: Dipicu setiap kali ada perubahan pada input (setiap karakter yang diketik atau dihapus). Ini berguna untuk logika yang berjalan secara real-time, seperti memfilter daftar atau menampilkan kekuatan password.
    TextFormField(
      onChanged: (value) {
        print('Current value: $value');
      },
    )
    
  • onFieldSubmitted: Dipicu ketika pengguna menekan tombol "submit" (seperti "enter" atau "done") pada keyboard. Ini berguna untuk memindahkan fokus ke field berikutnya atau langsung men-submit form.
    TextFormField(
      textInputAction: TextInputAction.next, // Tampilkan tombol 'next' di keyboard
      onFieldSubmitted: (value) {
        // Pindahkan fokus ke field berikutnya
        FocusScope.of(context).nextFocus();
      },
    )
    

Data Persistence dengan Shared Preferences

Setelah pengguna menutup aplikasi, semua state di dalam memori akan hilang. Data Persistence adalah proses menyimpan data secara permanen di penyimpanan lokal perangkat, sehingga data tersebut dapat diakses kembali saat aplikasi dibuka lagi.

1. Dasar Data Persistence

Flutter menawarkan beberapa cara untuk menyimpan data secara lokal, masing-masing dengan kegunaannya sendiri.

  • Shared Preferences:

    • Kegunaan: Untuk menyimpan data sederhana dalam format key-value pair. Sangat cocok untuk menyimpan preferensi pengguna (misalnya, mode gelap), status login, atau skor tertinggi dalam game.
    • Karakteristik: Cepat dan mudah digunakan, tetapi tidak cocok untuk data yang besar atau terstruktur secara kompleks.
    • Inilah fokus kita di bab ini.
  • Database Lokal (misalnya, SQLite):

    • Kegunaan: Untuk menyimpan data yang besar, kompleks, dan terstruktur. Misalnya, daftar kontak, riwayat transaksi, atau data aplikasi yang bisa diakses offline.
    • Karakteristik: Sangat kuat dan fleksibel, mendukung query yang kompleks, tetapi membutuhkan lebih banyak setup awal. Package populer untuk ini adalah sqflite.
  • Penyimpanan File (File Storage):

    • Kegunaan: Untuk membaca dan menulis file mentah langsung ke perangkat, seperti gambar, dokumen, log, atau data JSON.
    • Karakteristik: Memberikan kontrol penuh atas data, tetapi Anda bertanggung jawab untuk proses serialisasi dan deserialisasi data (mengubah objek menjadi string/byte dan sebaliknya).

2. Implementasi shared_preferences

Mari kita fokus pada cara termudah untuk memulai persistensi data: shared_preferences.

Langkah 1: Tambahkan Dependensi

Buka file pubspec.yaml Anda dan tambahkan shared_preferences di bawah dependencies.

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.0.15 # Gunakan versi terbaru

Setelah itu, jalankan flutter pub get di terminal Anda.

Langkah 2: Dapatkan Instance SharedPreferences

Untuk bisa membaca atau menulis data, Anda perlu mendapatkan instance dari SharedPreferences. Karena proses ini bersifat asynchronous (asinkron), kita menggunakan async/await.

import 'package:shared_preferences/shared_preferences.dart';

void main() async {
  // ...
  final prefs = await SharedPreferences.getInstance();
  // ...
}

Langkah 3: Menyimpan Data (Write)

SharedPreferences menyediakan method set untuk tipe data primitif: setBool, setInt, setDouble, dan setString.

// Mendapatkan instance
final prefs = await SharedPreferences.getInstance();

// Menyimpan data
await prefs.setBool('isLoggedIn', true);
await prefs.setInt('userLevel', 10);
await prefs.setString('username', 'john_doe');

Setiap method set mengembalikan Future<bool>, yang akan bernilai true jika penyimpanan berhasil.

Langkah 4: Membaca Data (Read)

Sama seperti set, ada method get untuk setiap tipe data: getBool, getInt, getDouble, dan getString.

Penting untuk diingat bahwa method get bisa mengembalikan null jika key yang Anda cari tidak ada. Oleh karena itu, Anda harus selalu menangani kasus null ini, biasanya dengan menyediakan nilai default menggunakan operator ??.

// Mendapatkan instance
final prefs = await SharedPreferences.getInstance();

// Membaca data dengan nilai default jika null
final bool isLoggedIn = prefs.getBool('isLoggedIn') ?? false;
final int userLevel = prefs.getInt('userLevel') ?? 1;
final String username = prefs.getString('username') ?? 'Guest';

print('Is Logged In: $isLoggedIn');
print('User Level: $userLevel');
print('Username: $username');

Langkah 5: Menghapus Data

Untuk menghapus key-value pair tertentu, gunakan method remove().

// Mendapatkan instance
final prefs = await SharedPreferences.getInstance();

// Menghapus satu key
await prefs.remove('userLevel');

Jika Anda ingin menghapus semua data di SharedPreferences (berguna untuk fungsi "logout" atau "reset aplikasi"), gunakan clear().

// Menghapus semua data
await prefs.clear();

Dengan shared_preferences, Anda dapat dengan mudah menambahkan kemampuan persistensi data sederhana ke aplikasi Anda, membuatnya lebih cerdas dan personal bagi pengguna.

Studi Kasus: Form Login dengan Persistence

Sekarang kita akan menggabungkan semua yang telah kita pelajari di bab ini: membuat form, memvalidasi input, dan menyimpan status login menggunakan shared_preferences.

Skenario Aplikasi:

  1. Saat aplikasi pertama kali dibuka, tampilkan halaman LoginPage.
  2. LoginPage memiliki form untuk email dan password.
  3. Validasi input: email harus valid dan password tidak boleh kosong.
  4. Jika login berhasil, simpan bool isLoggedIn = true ke shared_preferences dan arahkan pengguna ke HomePage.
  5. Saat aplikasi dibuka kembali, periksa nilai isLoggedIn. Jika true, langsung tampilkan HomePage dan lewati LoginPage.
  6. HomePage memiliki tombol "Logout" yang akan menghapus isLoggedIn dari shared_preferences dan mengembalikan pengguna ke LoginPage.

Langkah 1: Struktur Proyek dan Dependensi

Pastikan Anda sudah menambahkan shared_preferences di pubspec.yaml.

Buat 3 file di folder lib:

  • lib/main.dart
  • lib/login_page.dart
  • lib/home_page.dart

Langkah 2: main.dart (Entry Point)

File ini akan menjadi titik awal yang memeriksa status login.

lib/main.dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:form_persistence_example/home_page.dart';
import 'package:form_persistence_example/login_page.dart';

void main() async {
  // Pastikan Flutter binding sudah diinisialisasi
  WidgetsFlutterBinding.ensureInitialized();

  // Dapatkan instance SharedPreferences
  final prefs = await SharedPreferences.getInstance();
  // Baca status login, default-nya false jika tidak ada
  final bool isLoggedIn = prefs.getBool('isLoggedIn') ?? false;

  runApp(MyApp(isLoggedIn: isLoggedIn));
}

class MyApp extends StatelessWidget {
  final bool isLoggedIn;

  const MyApp({Key? key, required this.isLoggedIn}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Form & Persistence',
      // Tentukan halaman awal berdasarkan status login
      home: isLoggedIn ? HomePage() : LoginPage(),
      // Definisikan rute untuk navigasi
      routes: {
        '/login': (context) => LoginPage(),
        '/home': (context) => HomePage(),
      },
    );
  }
}

Langkah 3: login_page.dart

Ini adalah halaman yang berisi form login.

lib/login_page.dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class LoginPage extends StatefulWidget {
  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _login() async {
    // Validasi form
    if (_formKey.currentState!.validate()) {
      // Simpan status login ke SharedPreferences
      final prefs = await SharedPreferences.getInstance();
      await prefs.setBool('isLoggedIn', true);

      // Navigasi ke HomePage dan hapus semua rute sebelumnya
      Navigator.of(context).pushReplacementNamed('/home');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextFormField(
                controller: _emailController,
                decoration: InputDecoration(labelText: 'Email'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Email tidak boleh kosong';
                  }
                  // Validasi format email sederhana
                  if (!value.contains('@')) {
                    return 'Format email tidak valid';
                  }
                  return null;
                },
              ),
              SizedBox(height: 16),
              TextFormField(
                controller: _passwordController,
                decoration: InputDecoration(labelText: 'Password'),
                obscureText: true,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Password tidak boleh kosong';
                  }
                  return null;
                },
              ),
              SizedBox(height: 32),
              ElevatedButton(
                onPressed: _login,
                child: Text('Login'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Langkah 4: home_page.dart

Halaman utama yang ditampilkan setelah login berhasil.

lib/home_page.dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class HomePage extends StatelessWidget {
  void _logout(BuildContext context) async {
    // Hapus status login dari SharedPreferences
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove('isLoggedIn');

    // Navigasi ke LoginPage dan hapus semua rute sebelumnya
    Navigator.of(context).pushReplacementNamed('/login');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Page'),
        actions: [
          IconButton(
            icon: Icon(Icons.logout),
            onPressed: () => _logout(context),
          ),
        ],
      ),
      body: Center(
        child: Text('Selamat Datang! Anda berhasil login.'),
      ),
    );
  }
}

Dengan studi kasus ini, Anda telah berhasil mengintegrasikan dua konsep penting: menangani input pengguna dengan Form dan menjaga state aplikasi tetap ada dengan shared_preferences. Ini adalah pola yang sangat umum dan berguna di banyak aplikasi nyata.

Bab 6: State Management dan Implementasi CRUD dengan Collection

Selamat datang di Bab 6! Dalam pengembangan aplikasi Flutter yang kompleks, mengelola "state" atau data aplikasi adalah salah satu tantangan terbesar. Bab ini akan memperkenalkan Anda pada konsep State Management dan bagaimana kita dapat mengimplementasikannya untuk operasi CRUD (Create, Read, Update, Delete) sederhana menggunakan koleksi data di memori.

Memahami bagaimana data mengalir dan berubah dalam aplikasi Anda sangat penting untuk membangun UI yang responsif, efisien, dan mudah dipelihara. Kita akan mulai dengan dasar-dasar state, mengapa state management itu penting, dan kemudian langsung mempraktikkannya dengan membangun aplikasi daftar belanja sederhana.

Dalam bab ini, kita akan membahas:

  • Apa itu state dan mengapa perlu dikelola.
  • Pendekatan dasar untuk state management di Flutter.
  • Konsep operasi CRUD.
  • Implementasi CRUD menggunakan setState() dan List sebagai "database" in-memory.

Mari kita mulai membangun aplikasi yang lebih dinamis!

Dasar State Management

Dalam aplikasi Flutter, "state" adalah data yang dapat berubah selama aplikasi berjalan dan memengaruhi tampilan antarmuka pengguna (UI). Mengelola state ini secara efektif adalah kunci untuk membangun aplikasi yang responsif, efisien, dan mudah dipelihara.

1. Apa itu State?

Secara sederhana, state adalah segala data yang dapat dibaca oleh widget pada saat widget tersebut dibangun, dan yang dapat berubah seiring waktu. Ketika state sebuah widget berubah, Flutter akan membangun ulang (rebuild) widget tersebut untuk mencerminkan perubahan data.

Ada dua jenis state utama yang perlu Anda pahami:

a. Ephemeral State (Local State)

  • Definisi: State yang hanya relevan untuk satu widget tertentu dan tidak perlu dibagikan dengan widget lain di pohon widget.
  • Contoh:
    • Apakah sebuah Checkbox dicentang atau tidak.
    • Posisi saat ini dari sebuah PageView.
    • Nilai input dalam sebuah TextFormField sebelum disubmit.
  • Pengelolaan: Biasanya dikelola di dalam StatefulWidget itu sendiri menggunakan method setState().

b. App State (Global State)

  • Definisi: State yang perlu dibagikan di banyak bagian aplikasi, diakses oleh banyak widget, dan mungkin bertahan selama siklus hidup aplikasi.
  • Contoh:
    • Status autentikasi pengguna (sudah login atau belum).
    • Keranjang belanja dalam aplikasi e-commerce.
    • Preferensi pengguna (misalnya, tema gelap/terang).
    • Data yang diambil dari server.
  • Pengelolaan: Membutuhkan solusi state management yang lebih canggih (seperti Provider, BLoC, Riverpod, GetX, dll.) untuk menyediakannya ke seluruh aplikasi.

2. Mengapa State Management Penting?

Tanpa strategi state management yang baik, aplikasi Anda dapat menghadapi beberapa masalah:

  • Prop Drilling: Anda harus meneruskan data dari widget induk yang jauh ke widget anak yang dalam melalui banyak konstruktor perantara, meskipun widget perantara tersebut tidak menggunakan data tersebut. Ini membuat kode menjadi berantakan dan sulit diubah.
  • Rebuild yang Tidak Perlu: Jika state tidak dikelola dengan baik, perubahan kecil pada data dapat memicu rebuild seluruh pohon widget, yang dapat memengaruhi performa aplikasi.
  • Kode yang Sulit Dipelihara: Logika bisnis dan UI menjadi bercampur aduk, membuat kode sulit dibaca, diuji, dan diperbaiki.
  • Skalabilitas: Aplikasi akan sulit untuk diperluas dan dikembangkan lebih lanjut.

State management membantu memisahkan logika bisnis dari UI, membuat kode lebih modular, mudah diuji, dan lebih mudah dipelihara.

3. Pendekatan State Management Sederhana (tanpa package eksternal)

Flutter menyediakan beberapa mekanisme bawaan untuk mengelola state, terutama untuk Ephemeral State.

a. setState()

Ini adalah cara paling dasar dan paling sering digunakan untuk mengelola state di dalam StatefulWidget. Ketika Anda memanggil setState(), Flutter akan menandai widget tersebut sebagai "kotor" dan akan membangun ulang method build() dari widget tersebut (dan semua widget anaknya) pada frame berikutnya.

class MyCounter extends StatefulWidget {
  @override
  _MyCounterState createState() => _MyCounterState();
}

class _MyCounterState extends State<MyCounter> {
  int _count = 0; // State yang akan berubah

  void _incrementCounter() {
    setState(() { // Panggil setState untuk memberitahu Flutter bahwa state telah berubah
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'), // UI akan diperbarui saat _count berubah
        ElevatedButton(
          onPressed: _incrementCounter,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

b. InheritedWidget (Konsep Dasar)

InheritedWidget adalah mekanisme bawaan Flutter yang memungkinkan data untuk diwariskan ke bawah pohon widget secara efisien. Widget anak dapat mengakses data dari InheritedWidget terdekat di atasnya tanpa perlu meneruskan data melalui setiap konstruktor.

Meskipun sangat kuat, InheritedWidget seringkali digunakan sebagai dasar untuk membangun solusi state management yang lebih tinggi (seperti Provider), dan jarang digunakan secara langsung untuk aplikasi sehari-hari karena implementasinya yang sedikit lebih kompleks.

c. ChangeNotifier dan Provider (Konsep Dasar)

  • ChangeNotifier: Ini adalah kelas sederhana dari Flutter SDK (foundation library) yang dapat Anda extend untuk membuat objek yang dapat memberi tahu "pendengar" (listeners) ketika ada perubahan.
  • Provider: Ini adalah package populer yang dibangun di atas InheritedWidget dan ChangeNotifier. Provider menyederhanakan proses penyediaan ChangeNotifier (atau objek lain) ke pohon widget dan memungkinkan widget untuk "mendengarkan" perubahan pada objek tersebut.

Meskipun kita tidak akan mengimplementasikan Provider secara detail di bab ini, penting untuk mengetahui bahwa setState() adalah langkah awal yang baik, dan Provider adalah langkah berikutnya yang logis untuk mengelola App State yang lebih kompleks.

Implementasi CRUD dengan Collection

Setelah memahami dasar-dasar state management, sekarang kita akan menerapkan konsep tersebut untuk membangun aplikasi sederhana yang dapat melakukan operasi CRUD (Create, Read, Update, Delete) pada sebuah koleksi data di memori. Kita akan menggunakan List<String> sebagai "database" in-memory dan setState() untuk mengelola state.

1. Konsep CRUD

  • Create (Membuat): Menambahkan item baru ke dalam koleksi.
  • Read (Membaca): Menampilkan semua item yang ada dalam koleksi.
  • Update (Memperbarui): Mengubah item yang sudah ada dalam koleksi.
  • Delete (Menghapus): Menghilangkan item dari koleksi.

2. Studi Kasus: Aplikasi Daftar Belanja Sederhana (To-Do List)

Kita akan membuat aplikasi daftar belanja sederhana di mana pengguna dapat menambah, melihat, mengedit, dan menghapus item.

Langkah 1: Struktur Proyek

Buat file lib/main.dart sebagai entry point aplikasi.

Langkah 2: main.dart (Entry Point dan Aplikasi)

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Simple To-Do List',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: TodoListScreen(),
    );
  }
}

class TodoListScreen extends StatefulWidget {
  @override
  _TodoListScreenState createState() => _TodoListScreenState();
}

class _TodoListScreenState extends State<TodoListScreen> {
  final List<String> _todos = []; // Koleksi data kita
  final TextEditingController _textFieldController = TextEditingController();

  // Fungsi untuk menambahkan item
  void _addTodoItem(String title) {
    setState(() {
      _todos.add(title);
    });
    _textFieldController.clear(); // Bersihkan input setelah ditambahkan
  }

  // Fungsi untuk mengedit item
  void _editTodoItem(int index, String newTitle) {
    setState(() {
      _todos[index] = newTitle;
    });
  }

  // Fungsi untuk menghapus item
  void _deleteTodoItem(int index) {
    setState(() {
      _todos.removeAt(index);
    });
  }

  // Dialog untuk menambahkan/mengedit item
  Future<void> _displayTextInputDialog(BuildContext context, {int? index, String? currentTitle}) async {
    _textFieldController.text = currentTitle ?? ''; // Isi dengan judul saat ini jika mengedit

    return showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text(index == null ? 'Add New To-Do' : 'Edit To-Do'),
          content: TextField(
            controller: _textFieldController,
            decoration: InputDecoration(hintText: 'Enter your to-do item'),
            autofocus: true,
          ),
          actions: <Widget>[
            TextButton(
              child: Text('Cancel'),
              onPressed: () {
                Navigator.pop(context);
                _textFieldController.clear();
              },
            ),
            ElevatedButton(
              child: Text(index == null ? 'Add' : 'Save'),
              onPressed: () {
                if (_textFieldController.text.isNotEmpty) {
                  if (index == null) {
                    _addTodoItem(_textFieldController.text);
                  } else {
                    _editTodoItem(index, _textFieldController.text);
                  }
                  Navigator.pop(context);
                }
              },
            ),
          ],
        );
      },
    );
  }

  @override
  void dispose() {
    _textFieldController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('To-Do List'),
      ),
      body: _todos.isEmpty
          ? Center(child: Text('No to-do items yet! Add some below.'))
          : ListView.builder(
              itemCount: _todos.length,
              itemBuilder: (context, index) {
                return Card(
                  margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
                  child: ListTile(
                    title: Text(_todos[index]),
                    trailing: Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        IconButton(
                          icon: Icon(Icons.edit),
                          onPressed: () => _displayTextInputDialog(
                            context,
                            index: index,
                            currentTitle: _todos[index],
                          ),
                        ),
                        IconButton(
                          icon: Icon(Icons.delete),
                          onPressed: () => _deleteTodoItem(index),
                        ),
                      ],
                    ),
                  ),
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _displayTextInputDialog(context),
        child: Icon(Icons.add),
      ),
    );
  }
}

Penjelasan Kode:

  • _TodoListScreenState: Ini adalah State dari TodoListScreen yang menampung _todos (daftar item) dan _textFieldController.
  • _todos: Sebuah List<String> yang berfungsi sebagai koleksi data kita. Setiap perubahan pada list ini akan memicu setState().
  • _addTodoItem, _editTodoItem, _deleteTodoItem: Fungsi-fungsi ini memodifikasi _todos dan kemudian memanggil setState() untuk memberitahu Flutter agar membangun ulang UI.
  • _displayTextInputDialog: Fungsi ini menampilkan AlertDialog yang berisi TextField untuk input. Ini digunakan baik untuk menambahkan item baru maupun mengedit item yang sudah ada.
  • ListView.builder: Digunakan untuk menampilkan daftar item secara efisien. Setiap ListTile menampilkan satu item to-do.
  • IconButton (Edit & Delete): Tombol-tombol ini memicu fungsi _editTodoItem dan _deleteTodoItem masing-masing.
  • FloatingActionButton: Tombol ini memicu dialog untuk menambahkan item baru.

Dengan studi kasus ini, Anda telah berhasil mengimplementasikan operasi CRUD dasar pada koleksi data in-memory menggunakan setState() untuk state management. Ini adalah fondasi yang kuat untuk membangun aplikasi yang lebih kompleks dengan data dinamis.

Bab 7: Flutter Permissions

Selamat datang di Bab 7! Dalam pengembangan aplikasi mobile, seringkali kita membutuhkan akses ke fitur-fitur sensitif pada perangkat pengguna, seperti kamera, lokasi, mikrofon, atau penyimpanan. Untuk melindungi privasi pengguna, sistem operasi mobile (Android dan iOS) mengharuskan aplikasi untuk meminta izin (permissions) sebelum dapat mengakses fitur-fitur ini.

Bab ini akan memandu Anda memahami konsep dasar permissions di Flutter dan bagaimana cara mengimplementasikannya dengan benar menggunakan package permission_handler. Mengelola permissions dengan baik adalah kunci untuk membangun aplikasi yang aman, tepercaya, dan memberikan pengalaman pengguna yang baik.

Dalam bab ini, kita akan membahas:

  • Apa itu permissions dan mengapa mereka penting.
  • Jenis-jenis permissions dan alur permintaannya.
  • Cara mengkonfigurasi permissions di Android dan iOS.
  • Implementasi praktis menggunakan package permission_handler untuk meminta dan menangani status izin.
  • Studi kasus sederhana untuk meminta izin kamera.

Mari kita pastikan aplikasi kita menghormati privasi pengguna!

Dasar Flutter Permissions

Memahami konsep dasar permissions adalah langkah pertama yang krusial sebelum mengimplementasikannya di aplikasi Flutter Anda. Permissions adalah mekanisme keamanan yang melindungi data dan fungsionalitas sensitif pada perangkat pengguna.

1. Apa itu Permissions?

Permissions adalah izin yang harus diberikan oleh pengguna kepada aplikasi agar aplikasi tersebut dapat mengakses sumber daya atau fungsionalitas tertentu pada perangkat. Sumber daya ini bisa berupa:

  • Hardware: Kamera, mikrofon, sensor, Bluetooth.
  • Data Pribadi: Lokasi, kontak, kalender, galeri foto.
  • Jaringan: Akses internet (meskipun ini seringkali dianggap "normal" permission).

Mengapa Aplikasi Membutuhkan Izin?

  • Keamanan: Mencegah aplikasi jahat mengakses data sensitif tanpa sepengetahuan pengguna.
  • Privasi Pengguna: Memberikan kontrol penuh kepada pengguna atas data dan perangkat mereka. Pengguna dapat memutuskan fitur mana yang boleh diakses oleh aplikasi.
  • Pengalaman Pengguna: Aplikasi yang meminta izin secara transparan dan pada waktu yang tepat akan membangun kepercayaan pengguna.

2. Jenis-jenis Permissions

Sistem operasi mobile mengkategorikan permissions ke dalam beberapa jenis, yang memengaruhi bagaimana dan kapan izin tersebut harus diminta.

a. Install-time Permissions (Normal Permissions)

  • Karakteristik: Izin ini diberikan secara otomatis oleh sistem operasi saat aplikasi diinstal. Pengguna tidak akan melihat dialog permintaan izin untuk jenis ini.
  • Contoh: INTERNET (akses internet), ACCESS_NETWORK_STATE (melihat status jaringan).
  • Risiko: Umumnya tidak menimbulkan risiko privasi yang signifikan bagi pengguna.

b. Runtime Permissions (Dangerous Permissions)

  • Karakteristik: Izin ini memerlukan konfirmasi eksplisit dari pengguna saat aplikasi berjalan (runtime), yaitu saat aplikasi pertama kali mencoba mengakses fungsionalitas yang dilindungi. Pengguna akan melihat dialog pop-up untuk menyetujui atau menolak izin.
  • Contoh: CAMERA (akses kamera), ACCESS_FINE_LOCATION (akses lokasi akurat), READ_CONTACTS (membaca kontak), RECORD_AUDIO (merekam audio).
  • Risiko: Berpotensi mengakses data sensitif pengguna atau mengontrol fungsionalitas perangkat yang dapat memengaruhi privasi.

3. Alur Permintaan Izin (Request Permission Flow)

Ketika aplikasi Anda membutuhkan runtime permission, Anda harus mengikuti alur standar:

  1. Periksa Status Izin Saat Ini: Sebelum meminta izin, selalu periksa apakah izin sudah diberikan, ditolak, atau ditolak permanen. Ini mencegah Anda meminta izin yang sudah ada atau berulang kali mengganggu pengguna.
  2. Jika Belum Diberikan, Minta Izin: Jika status izin menunjukkan bahwa izin belum diberikan, tampilkan dialog permintaan izin kepada pengguna.
  3. Tangani Respons Pengguna:
    • Diberikan (Granted): Aplikasi dapat melanjutkan fungsionalitas yang membutuhkan izin tersebut.
    • Ditolak (Denied): Pengguna menolak izin. Anda mungkin perlu menjelaskan mengapa izin tersebut diperlukan dan menawarkan untuk meminta lagi di lain waktu.
    • Ditolak Permanen (Permanently Denied): Pengguna menolak izin dan memilih "Jangan tanya lagi". Dalam kasus ini, Anda tidak dapat lagi meminta izin secara langsung. Anda harus mengarahkan pengguna ke pengaturan aplikasi agar mereka dapat mengaktifkannya secara manual.

4. Platform-Specific Permissions

Meskipun Flutter menyediakan API lintas platform, konfigurasi awal untuk permissions masih perlu dilakukan secara spesifik untuk setiap platform.

a. Android

Anda harus mendeklarasikan semua izin yang dibutuhkan aplikasi Anda di file AndroidManifest.xml. File ini biasanya terletak di android/app/src/main/AndroidManifest.xml.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Contoh izin kamera -->
    <uses-permission android:name="android.permission.CAMERA"/>
    <!-- Contoh izin lokasi -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <!-- ... izin lainnya ... -->
</manifest>

b. iOS

Untuk iOS, Anda perlu menambahkan deskripsi penggunaan izin ke file Info.plist. File ini biasanya terletak di ios/Runner/Info.plist. Deskripsi ini akan ditampilkan kepada pengguna saat aplikasi meminta izin.

<dict>
    <!-- ... properti lainnya ... -->
    <key>NSCameraUsageDescription</key>
    <string>Aplikasi ini membutuhkan akses kamera untuk mengambil foto.</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>Aplikasi ini membutuhkan akses lokasi saat digunakan untuk menampilkan peta.</string>
    <!-- ... deskripsi izin lainnya ... -->
</dict>

Tanpa deskripsi ini, aplikasi Anda mungkin akan crash saat mencoba meminta izin di iOS.

Dengan pemahaman ini, Anda siap untuk mulai mengimplementasikan permintaan izin di aplikasi Flutter Anda.

Implementasi Flutter Permissions

Setelah memahami konsep dasar permissions, sekarang kita akan mengimplementasikannya di aplikasi Flutter menggunakan package permission_handler. Package ini sangat direkomendasikan karena menyediakan API yang konsisten untuk menangani permissions di Android dan iOS.

1. Menggunakan Package permission_handler

a. Menambahkan Dependensi

Buka file pubspec.yaml Anda dan tambahkan permission_handler di bawah dependencies.

dependencies:
  flutter:
    sdk: flutter
  permission_handler: ^10.2.0 # Gunakan versi terbaru

Setelah itu, jalankan flutter pub get di terminal Anda.

b. Konfigurasi Platform-Specific

Seperti yang dibahas sebelumnya, Anda perlu mengkonfigurasi file manifest/info untuk setiap platform.

  • Android (android/app/src/main/AndroidManifest.xml): Tambahkan izin yang diperlukan di dalam tag <manifest>. Contoh untuk kamera:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <uses-permission android:name="android.permission.CAMERA"/>
        <!-- Tambahkan izin lain yang dibutuhkan di sini -->
        <application ...>
            <!-- ... -->
        </application>
    </manifest>
    
  • iOS (ios/Runner/Info.plist): Tambahkan deskripsi penggunaan izin di dalam tag <dict>. Contoh untuk kamera:

    <dict>
        <key>NSCameraUsageDescription</key>
        <string>Aplikasi ini membutuhkan akses kamera untuk mengambil foto.</string>
        <!-- Tambahkan deskripsi izin lain yang dibutuhkan di sini -->
    </dict>
    

    Pastikan untuk mengganti string deskripsi dengan penjelasan yang relevan untuk pengguna Anda.

2. Langkah-langkah Implementasi dengan permission_handler

a. Import Package

import 'package:permission_handler/permission_handler.dart';

b. Periksa Status Izin

Anda dapat memeriksa status izin tunggal atau beberapa izin sekaligus.

// Periksa status izin kamera
PermissionStatus cameraStatus = await Permission.camera.status;
print('Camera permission status: $cameraStatus');

// Periksa status izin lokasi
PermissionStatus locationStatus = await Permission.location.status;
print('Location permission status: $locationStatus');

c. Minta Izin

Jika izin belum diberikan, Anda dapat memintanya. Method request() akan menampilkan dialog permintaan izin kepada pengguna.

PermissionStatus status = await Permission.camera.request();

if (status.isGranted) {
  print('Camera permission granted');
} else if (status.isDenied) {
  print('Camera permission denied');
} else if (status.isPermanentlyDenied) {
  print('Camera permission permanently denied');
}

d. Tangani Hasil Permintaan

Berdasarkan PermissionStatus yang dikembalikan:

  • isGranted: Izin diberikan. Lanjutkan fungsionalitas.
  • isDenied: Izin ditolak. Anda bisa menjelaskan mengapa izin diperlukan dan meminta lagi.
  • isPermanentlyDenied: Izin ditolak permanen. Pengguna harus mengaktifkannya secara manual dari pengaturan aplikasi.

e. Buka Pengaturan Aplikasi

Jika izin ditolak permanen, Anda dapat mengarahkan pengguna langsung ke pengaturan aplikasi mereka.

if (status.isPermanentlyDenied) {
  // Tampilkan dialog atau pesan kepada pengguna
  // Lalu arahkan ke pengaturan aplikasi
  openAppSettings();
}

3. Studi Kasus: Aplikasi Kamera Sederhana

Mari kita buat aplikasi sederhana yang meminta izin kamera dan menampilkan statusnya.

Langkah 1: Buat Proyek Baru

Buat proyek Flutter baru.

Langkah 2: main.dart

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Camera Permission Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CameraPermissionScreen(),
    );
  }
}

class CameraPermissionScreen extends StatefulWidget {
  @override
  _CameraPermissionScreenState createState() => _CameraPermissionScreenState();
}

class _CameraPermissionScreenState extends State<CameraPermissionScreen> {
  String _permissionStatusText = 'Checking camera permission...';

  @override
  void initState() {
    super.initState();
    _checkAndRequestCameraPermission();
  }

  Future<void> _checkAndRequestCameraPermission() async {
    PermissionStatus status = await Permission.camera.status;

    if (status.isGranted) {
      setState(() {
        _permissionStatusText = 'Camera permission GRANTED. You can use the camera!';
      });
    } else if (status.isDenied) {
      // Izin ditolak, coba minta lagi
      status = await Permission.camera.request();
      if (status.isGranted) {
        setState(() {
          _permissionStatusText = 'Camera permission GRANTED after request.';
        });
      } else {
        setState(() {
          _permissionStatusText = 'Camera permission DENIED.';
        });
      }
    } else if (status.isPermanentlyDenied) {
      setState(() {
        _permissionStatusText = 'Camera permission PERMANENTLY DENIED. Please enable it from app settings.';
      });
    } else if (status.isRestricted) {
      setState(() {
        _permissionStatusText = 'Camera permission RESTRICTED.';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Camera Permission Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              _permissionStatusText,
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 18),
            ),
            SizedBox(height: 20),
            if (_permissionStatusText.contains('PERMANENTLY DENIED'))
              ElevatedButton(
                onPressed: () {
                  openAppSettings(); // Buka pengaturan aplikasi
                },
                child: Text('Open App Settings'),
              ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _checkAndRequestCameraPermission,
              child: Text('Re-check Permission'),
            ),
          ],
        ),
      ),
    );
  }
}

Penjelasan Kode:

  • _permissionStatusText: Variabel state untuk menampilkan status izin saat ini.
  • initState(): Memanggil _checkAndRequestCameraPermission() saat widget pertama kali dibuat.
  • _checkAndRequestCameraPermission():
    • Pertama, memeriksa status izin kamera.
    • Jika isGranted, langsung update UI.
    • Jika isDenied, mencoba meminta izin lagi.
    • Jika isPermanentlyDenied, menampilkan pesan dan tombol untuk membuka pengaturan aplikasi.
  • openAppSettings(): Fungsi dari permission_handler yang akan membuka pengaturan aplikasi perangkat, memungkinkan pengguna untuk mengaktifkan izin secara manual.
  • ElevatedButton "Re-check Permission": Memungkinkan pengguna untuk memeriksa ulang status izin setelah mereka mungkin mengubahnya di pengaturan.

Dengan studi kasus ini, Anda telah berhasil mengimplementasikan alur permintaan izin yang lengkap untuk kamera, termasuk penanganan berbagai status izin dan mengarahkan pengguna ke pengaturan aplikasi jika diperlukan. Ini adalah pola yang dapat Anda adaptasi untuk permissions lainnya.

Bab 8: Mengkonsumsi API (Consume API)

Di dunia nyata, aplikasi yang berguna seringkali adalah aplikasi yang mampu "berbicara" dengan layanan lain — mengambil daftar, mengirim perubahan, atau memperbarui antarmuka berdasarkan data jarak jauh. Bab ini dimaksudkan untuk membawa Anda melewati perjalanan tersebut: bukan hanya sebagai kumpulan perintah, melainkan sebagai latihan merancang alur data yang masuk dan keluar sambil menjaga pengalaman pengguna tetap mulus ketika koneksi tidak sempurna. Contoh dan studi kasus yang disertakan akan membantu Anda memahami pola pikir yang diperlukan agar aplikasi tetap responsif, mudah diuji, dan mudah dirawat.

Bab ini membahas bagaimana aplikasi Flutter/Dart berkomunikasi dengan service backend melalui HTTP: dari konsep dasar, request/response, parsing JSON, sampai implementasi aplikasi nyata.

Di akhir bab ini Anda akan bisa:

  • Memahami arsitektur request HTTP dan format data umum (JSON)
  • Menggunakan paket http (atau alternatif seperti Dio) untuk melakukan GET/POST/PUT/DELETE
  • Membuat model data (Dart class) dari JSON dan sebaliknya
  • Menangani error, timeouts (otentikasi akan dibahas di bab terpisah)
  • Mengimplementasikan studi kasus aplikasi yang mengambil data dari API publik dan melakukan operasi CRUD sederhana

Struktur bab:

Saran: ikuti studi kasus langkah demi langkah sambil mengetik dan menjalankan contoh. Gunakan emulator atau perangkat fisik untuk melihat hasilnya.

8.1 Konsep: Apa itu API dan bagaimana aplikasi mengkonsumsinya

API (Application Programming Interface) adalah sekumpulan aturan yang memungkinkan satu aplikasi berkomunikasi dengan aplikasi lain. Di konteks aplikasi mobile/web, API biasanya merujuk ke layanan backend yang menyediakan data melalui protokol HTTP dan format data seperti JSON.

Konsep penting:

  • Endpoint: URL yang mewakili resource (contoh: https://api.example.com/posts)
  • Method HTTP: GET (ambil data), POST (kirim data baru), PUT/PATCH (ubah data), DELETE (hapus)
  • Status Code: angka yang menunjukkan hasil (200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Server Error)
  • Payload/Body: data yang dikirim atau diterima, umumnya dalam format JSON
  • Headers: meta-data request seperti Content-Type, Authorization (untuk token)

Alur konsumsi API di aplikasi Flutter:

  1. Aplikasi membuat request HTTP ke endpoint
  2. Server memproses dan mengirim response (status + body)
  3. Aplikasi menerima response, melakukan parsing JSON ke model data
  4. UI di-refresh untuk menampilkan data

Pertimbangan desain:

  • Asinkron: operasi jaringan memakan waktu — gunakan Future, async/await, atau stream
  • Error handling: network errors, parsing errors, dan error server harus ditangani
  • Caching & offline: simpan data lokal (SharedPreferences / Hive / SQLite) untuk pengalaman offline
  • Keamanan: jangan simpan token sensitif secara terbuka; gunakan secure storage bila perlu

8.2 HTTP di Dart: async, Future, dan paket http

Paket yang paling sederhana untuk HTTP di Dart/Flutter adalah package:http. Untuk proyek yang lebih besar dan fitur lebih lengkap dapat digunakan dio.

Contoh dependensi pada pubspec.yaml:

dependencies:
  http: ^0.13.0

Contoh membuat GET request sederhana:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> fetchPosts() async {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  final response = await http.get(url);

  if (response.statusCode == 200) {
    final List data = jsonDecode(response.body);
    print('Diterima ${data.length} item');
  } else {
    throw Exception('Gagal fetch: ${response.statusCode}');
  }
}

Catatan:

  • Gunakan Uri.parse untuk membangun URL; ini membantu query parameters dan encoding.
  • Jangan memanggil fungsi jaringan di build() — gunakan initState, FutureBuilder, Provider, Riverpod atau state management lain.
  • Tambahkan timeout jika perlu: await http.get(url).timeout(Duration(seconds: 10))

Contoh POST request:

Future<void> createPost(Map<String, dynamic> payload) async {
  final url = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  final response = await http.post(url,
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode(payload),
  );

  if (response.statusCode == 201) {
    print('Berhasil membuat post');
  } else {
    throw Exception('Gagal membuat post: ${response.statusCode}');
  }
}

8.3 Parsing JSON ke Model Dart

Untuk memudahkan kerja dengan data, sebaiknya buat kelas model yang merepresentasikan struktur JSON.

Contoh JSON dari endpoint posts:

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit..."
}

Model Dart sederhana:

class Post {
  final int userId;
  final int id;
  final String title;
  final String body;

  Post({required this.userId, required this.id, required this.title, required this.body});

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      userId: json['userId'] as int,
      id: json['id'] as int,
      title: json['title'] as String,
      body: json['body'] as String,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'userId': userId,
      'id': id,
      'title': title,
      'body': body,
    };
  }
}

Tips:

  • Untuk proyek besar, gunakan generator seperti json_serializable untuk mengurangi boilerplate.
  • Validasi tipe dan nilai null safety penting: gunakan as Type? dan fallback bila perlu.

8.4 Penanganan Error

Network code rentan terhadap berbagai kegagalan. Berikut beberapa pola penanganan error:

  • Timeouts: gunakan .timeout(Duration(...)) dan tangani TimeoutException.
  • Network errors: SocketException jika tidak ada koneksi.
  • Response error: periksa statusCode dan parsing message dari body.
  • Retries: untuk operasi idempotent, gunakan retry dengan exponential backoff.

Contoh penanganan sederhana:

import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;

Future<String> safeGet(Uri url) async {
  try {
    final response = await http.get(url).timeout(Duration(seconds: 10));
    if (response.statusCode == 200) return response.body;
    throw HttpException('Server returned ${response.statusCode}');
  } on TimeoutException {
    throw Exception('Request timed out');
  } on SocketException {
    throw Exception('No internet connection');
  }
}

Catatan: otentikasi (token, OAuth, dsb.) tidak dibahas di bab ini. Kita akan membahas otentikasi di bab terpisah.

8.5 Studi Kasus: Aplikasi Sederhana Konsumsi API (Posts)

Di studi kasus ini kita akan membuat aplikasi Flutter sederhana yang:

  • Mengambil daftar post dari https://jsonplaceholder.typicode.com/posts
  • Menampilkan daftar di ListView
  • Menampilkan detail saat item dipilih
  • Menambahkan post baru (POST)

Struktur file contoh singkat:

  • lib/
    • models/post.dart
    • services/api_service.dart
    • screens/home_screen.dart
    • screens/detail_screen.dart

Contoh models/post.dart:

class Post {
  final int userId;
  final int id;
  final String title;
  final String body;

  Post({required this.userId, required this.id, required this.title, required this.body});

  factory Post.fromJson(Map<String, dynamic> json) => Post(
    userId: json['userId'],
    id: json['id'],
    title: json['title'],
    body: json['body'],
  );
}

Contoh services/api_service.dart:

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/post.dart';

class ApiService {
  final base = 'https://jsonplaceholder.typicode.com';

  Future<List<Post>> fetchPosts() async {
    final res = await http.get(Uri.parse('
      '$base/posts'
    ));
    if (res.statusCode == 200) {
      final List json = jsonDecode(res.body);
      return json.map((e) => Post.fromJson(e)).toList();
    }
    throw Exception('Gagal fetch posts');
  }

  Future<Post> createPost(Post p) async {
    final res = await http.post(Uri.parse('$base/posts'), headers: {
      'Content-Type': 'application/json'
    }, body: jsonEncode({'title': p.title, 'body': p.body, 'userId': p.userId}));

    if (res.statusCode == 201) return Post.fromJson(jsonDecode(res.body));
    throw Exception('Gagal membuat post');
  }
}

Saran: implementasikan UI dengan FutureBuilder untuk fetch awal, dan Navigator.push ke detail screen.

Langkah verifikasi:

  1. Tambahkan http ke pubspec.yaml
  2. Jalankan flutter pub get
  3. Run aplikasi di emulator dan pastikan daftar posts muncul