Cheatsheet UAS Emerging Technology

⚠️ PERSIAPAN WAJIB (JANGAN SKIP)
1. IP Address (API URL):
  o Emulator: http://10.0.2.2/flutter/... (Jangan Localhost!)
  o HP Fisik: http://192.168.x.x/flutter/... (Cek IP Laptop, satu WiFi)
2. pubspec.yaml: http: ^1.x.x, shared_preferences: ^2.x.x
3. Permission (Android): Tambah <uses-permission android:name="android.permission.INTERNET" /> di AndroidManifest.xml.

WEEK 8: READ DATA (HTTP GET)

Logika: Flutter minta data ke PHP -> PHP ambil dari MySQL -> PHP ubah jadi JSON -> Flutter terima JSON -> Flutter ubah jadi Objek (Parsing).
Analogi: Bayangkan PHP itu koki, MySQL itu kulkas. Flutter (Pelanggan) pesan makanan. Koki ambil bahan di kulkas, dimasak jadi JSON (hidangan), lalu dikasih ke Pelanggan.

1. PHP Backend (Penyedia Data)

Wajib header CORS agar Flutter diizinkan akses. Output harus JSON Array.
header("Access-Control-Allow-Origin: *"); // PENTING: Izin akses global
$conn = new mysqli("localhost", "root", "", "db_movie");
$sql = "SELECT * FROM movie";
$result = $conn->query($sql);
$data = [];
while($row = $result->fetch_assoc()) {
    array_push($data, $row); // Masukkan tiap baris DB ke array
}
echo json_encode($data); // Ubah jadi Teks JSON

2. Dart Model (Wadah Data)

Gunakan factory sebagai "pabrik" pengolah JSON mentah menjadi variabel aman.
Penting: factory memastikan data JSON yang acak-acakan diubah jadi objek rapi yang punya tipe data jelas (Int, String).
class Movie {
  int id; String title; String overview;
  Movie({required this.id, required this.title, required this.overview});

  factory Movie.fromJson(Map<String, dynamic> json) {
    return Movie(
      id: json['movie_id'] as int,      // Casting: Paksa jadi int
      title: json['title'] as String,   // Casting: Paksa jadi String
      overview: json['overview'] as String,
    );
  }
}

3. Fetch Data (Kurir)

Gunakan Future karena ambil data butuh waktu (Asynchronous).
Future<String> fetchData() async {
  final response = await http.get(Uri.parse("http://.../api_list.php"));
  if (response.statusCode == 200) return response.body; // Kembalikan mentahan body
  else throw Exception('Gagal koneksi API');
}
// UI: Panggil di initState(), simpan ke List<Movie>, tampilkan pakai ListView.builder

WEEK 9: SEARCH & NESTED JSON

Logika: Mengirim data (POST) untuk filter, dan menangani data bersarang (1 Film punya banyak Genre).

1. Parameterized Query (PHP - Anti Hack)

Jangan pakai WHERE title = '$cari' (Bahaya SQL Injection). Pakai prepare.
Kenapa? Kalau pakai '$cari', hacker bisa masukin kode jahat. Pakai ? (tanda tanya) biar database tahu itu cuma data biasa.
$cari = "%" . $_POST['cari'] . "%";
// Tambah wildcard % untuk LIKE
// Siapkan template dengan tanda tanya (?)
$stmt = $conn->prepare("SELECT * FROM movie WHERE title LIKE ?");
$stmt->bind_param("s", $cari); // "s" = string (Tipe data parameter)
$stmt->execute();
$result = $stmt->get_result();

2. HTTP POST (Kirim Data)

Kirim parameter dalam bentuk Map di properti body.
http.post(
  Uri.parse(".../api_cari.php"), 
  body: {'cari': 'batman'} // Key 'cari' akan ditangkap $_POST['cari']
);

3. Nested JSON (Logika Data Beranak)

PHP: Loop Film -> Di dalam loop, query ke tabel Genre -> Masukkan hasilnya ke Array Film.
Analogi: Seperti absen kelas. Guru panggil nama Murid (Film), lalu tanya "Kamu ekskulnya apa aja?" (Genre). Dicatat di samping nama murid, baru lanjut ke murid berikutnya.
while($row = $res->fetch_assoc()) {
    $id = $row['movie_id'];
    // Query Anak
    $res_genre = $conn->query("SELECT * FROM genres WHERE movie_id=$id");
    $genres = [];
    while($g = $res_genre->fetch_assoc()) { $genres[] = $g; }

    $row['genres'] = $genres; // Masukkan anak ke dalam array bapak
    array_push($final_data, $row);
}
Dart: Tambahkan List? di Model.
List<dynamic>? genres; // Nullable (jaga-jaga kalau kosong)
// Di factory:
genres: json['genres'] // Dart otomatis baca array JSON jadi List

WEEK 10: FORM INPUT

Logika: Form butuh "Remote Control" (GlobalKey) untuk mengecek apakah semua inputan user sudah valid sebelum dikirim ke server.
GlobalKey: Bayangkan ini remote TV. TV-nya adalah Form. Kamu pegang remote-nya. Pas tombol ditekan, kamu pencet remote buat suruh TV ngecek dirinya sendiri.

1. Setup GlobalKey

final _formKey = GlobalKey<FormState>(); // Remote Control Validasi
// Di build():
Form(key: _formKey, child: Column(children: [...]))

2. Widget Input & Validator

Fungsi validator akan jalan otomatis saat tombol submit ditekan.
TextFormField(
  controller: _titleController,
  decoration: InputDecoration(labelText: "Judul Film"),
  validator: (value) {
    if (value == null || value.isEmpty) return 'Wajib diisi!'; // Error message
    if (value.length < 3) return 'Minimal 3 huruf';
    return null; // Null artinya Valid (Lolos)
  },
)

3. Date Picker

DatePicker tidak bisa diketik manual, jadi gunakan onTap pada TextField readOnly.
TextFormField(
  controller: _dateController,
  readOnly: true, // Biar keyboard gak muncul
  decoration: InputDecoration(labelText: "Tanggal Rilis"),
  onTap: () async {
    DateTime? picked = await showDatePicker(
      context: context, initialDate: DateTime.now(),
      firstDate: DateTime(2000), lastDate: DateTime(2100)
    );
    if (picked != null) {
      // Ambil 10 karakter pertama (YYYY-MM-DD)
      _dateController.text = picked.toString().substring(0, 10);
    }
  }
)

4. Submit & Feedback (Pesan Sukses)

Cek validasi -> Kirim -> Cek Respons -> Tampilkan Pesan -> Tutup Halaman.
void submit() async {
  if (_formKey.currentState!.validate()) { // 1. Cek Validasi
    final response = await http.post(url, body: { ... }); // 2. Kirim
    if (response.statusCode == 200) {
      Map json = jsonDecode(response.body);
      if (json['result'] == 'success') {
         if (!mounted) return; // Cek layar masih aktif
         // 3. Tampilkan Feedback
         ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Data berhasil disimpan!'))
         );
         // 4. Tutup Halaman
         Navigator.pop(context); 
      }
    }
  }
}

WEEK 11: EDIT DATA & NAVIGASI

Logika: Edit = Input Form yang sudah terisi data lama. Navigasi balik harus me-refresh data.

1. Pre-fill Data (Isi Otomatis)

Lakukan di initState agar form terisi saat halaman dibuka.
@override
void initState() {
  super.initState();
  _titleCtrl.text = widget.movie.title; // Isi dari data yang dikirim
  _id = widget.movie.id;
}

2. Dropdown (Combobox)

Syarat: Tipe data variable penampung (_selectedGenre) harus SAMA dengan value menu item.
Warning: Kalau variabel penampung tipe int, value di menu item JANGAN String. Harus sama-sama int biar gak crash.
int? _selectedGenre; // Penampung ID terpilih
DropdownButton(
  value: _selectedGenre,
  hint: Text("Pilih Genre"),
  items: listGenre.map((item) {
    // Map data API ke Menu Item
    return DropdownMenuItem(value: item.id, child: Text(item.name));
  }).toList(),
  onChanged: (val) {
    setState(() { _selectedGenre = val as int; }); // Update UI
  }
)

3. Refresh Logic (Navigasi)

Gunakan .then() pada Navigator Push untuk mendeteksi saat user kembali.
Navigator.push(context, MaterialPageRoute(...))
  .then((value) { 
     // Jalan otomatis saat user kembali dari halaman Edit
     setState(() { bacaData(); }); // Refresh List
  });

WEEK 12: GAMBAR (UPLOAD & VIEW)

Logika: Gambar -> Ubah ke Bytes -> Ubah ke String (Base64) -> Kirim Teks ke Server -> Server Decode balik jadi File. Setup: image_picker: ^1.x.x (Cek Lampiran untuk Izin iOS!)

1. Pick Image & Convert Base64 (Dart)

Uint8List? _imgBytes; // Simpan gambar di memori
Future pickImage() async {
  final picker = ImagePicker();
  final img = await picker.pickImage(source: ImageSource.gallery);
  if (img != null) {
    _imgBytes = await img.readAsBytes(); // Convert File -> Bytes
    setState((){});
  }
}
// Upload Function
String base64 = base64Encode(_imgBytes!); // Bytes -> String Panjang
http.post(url, body: {'id': '1', 'image': base64});

2. PHP Handle Upload & Scandir

Decode string base64 kembali menjadi file fisik .jpg.
// Upload
$data = base64_decode($_POST['image']);
if (!is_dir("images/$id")) mkdir("images/$id", 0777, true);
// Buat folder
file_put_contents("images/$id/" . time() . ".jpg", $data); // Simpan file

// Read List Gambar (scandir) untuk ditampilkan lagi
$dir = "images/$id";
$images = [];
if (is_dir($dir)) {
    $files = scandir($dir); // Baca isi folder
    foreach ($files as $f) {
        // Ambil hanya file jpg/png
        if ($f != "." && $f != ".." && preg_match('/\.(jpg|png)$/i', $f)) {
            $images[] = "https://.../images/$id/$f";
        }
    }
}

WEEK 13: SQLITE (OFFLINE CART)

Logika: Database kecil di HP. Menggunakan pola Singleton agar koneksi DB hanya satu (mencegah corrupt). Setup: sqflite, path

1. Database Singleton

Singleton: Pola koding untuk memastikan "Pintu Masuk" ke database cuma ada satu. Biar gak tabrakan kalau ada 2 proses mau masuk barengan.
class DB {
  static final DB instance = DB._init(); // Singleton
  static Database? _db;
  DB._init();

  Future<Database> get db async {
    if (_db != null) return _db!; // Kalau sudah ada, pakai yg lama
    // Kalau belum, buat baru
    String path = join(await getDatabasesPath(), 'cart.db');
    _db = await openDatabase(path, version: 1, onCreate: (db, v) {
      db.execute('CREATE TABLE cart(id INTEGER PRIMARY KEY, title TEXT, qty INTEGER)');
    });
    return _db!;
  }
}

2. Checkout Sync (Format String)

Trik mengirim list belanja sekaligus dalam 1 request: Gabung jadi String.
Dart: Loop cart, gabung string: id,qty|id,qty| -> "1,2|5,1|"
// Dart: Gabung String
String s = "";
cart.forEach((e) => s += "${e['id']},${e['qty']}|");

// PHP: Pecah string (explode)
$items = $_POST['items']; 
$rows = explode("|", $items); // Pecah per barang
foreach($rows as $row) {
    if(!empty($row)) {
        $detail = explode(",", $row); // Pecah id dan qty -> [0]=>id, [1]=>qty
        $conn->query("INSERT INTO sales_det VALUES ($sales_id, $detail[0], $detail[1])");
    }
}

WEEK 14: MAP & LOCATION

Logika: Ambil GPS (X,Y). Gunakan Timer untuk update posisi. Gunakan Math atan untuk putar arah ikon. Setup: location, flutter_map, latlong2

1. Timer & Init (Looping Update)

Timer? _timer;
@override
void initState() {
  super.initState();
  // Jalankan fungsi update setiap 1 detik
  _timer = Timer.periodic(Duration(seconds: 1), (t) => updatePosisi());
}
@override
void dispose() {
  _timer?.cancel(); // MATIKAN TIMER saat keluar (Penting!)
  super.dispose();
}

2. Logic Rotasi (Animasi Mobil)

Rumus: atan(deltaX / deltaY).
Penjelasan: Kita pakai rumus matematika atan (Arc Tangent) untuk mencari sudut kemiringan. Tapi hati-hati, atan cuma bisa hitung putaran setengah lingkaran. Kalau mobil jalan ke bawah, harus ditambah 180 derajat (PI).
// Hitung selisih posisi
double dy = yBaru - yLama;
double dx = xBaru - xLama;
double angle = atan(dx/dy); // Hitung sudut radian

// Koreksi Error: Jika NaN (Diam) atau Arah Bawah
if (angle.isNaN) angle = _prevAngle; // Jangan ubah kalau diam
else {
  if (dy < 0) angle += pi; // Putar 180 derajat jika jalan ke bawah (Y negatif)
}

// UI:
Transform.rotate(angle: angle, child: Icon(Icons.local_taxi))

LAMPIRAN: SAFETY NETS (PENYELAMAT NYAWA)

1. LOGIC HAPUS (DELETE)

Data berelasi tidak bisa langsung dihapus. Hapus anaknya dulu.
// PHP: Hapus Genre dulu, baru Movie
$conn->query("DELETE FROM movie_genres WHERE movie_id = $id");
$conn->query("DELETE FROM movie WHERE movie_id = $id");

// Dart: Hapus dari list lokal biar UI update instan
movies.removeWhere((item) => item.id == id);
setState((){});

2. SESSION (LOGIN)

Simpan user_id agar bisa dipakai saat checkout/komentar.
final prefs = await SharedPreferences.getInstance();
prefs.setString('uid', '123'); // Login: Simpan
String uid = prefs.getString('uid') ?? ''; // Pakai: Ambil (Default '')
prefs.clear(); // Logout: Hapus

3. IZIN iOS (Info.plist) - Wajib untuk iPhone

<key>NSPhotoLibraryUsageDescription</key><string>Butuh akses galeri untuk upload</string>
<key>NSCameraUsageDescription</key><string>Butuh akses kamera</string>
<key>NSLocationWhenInUseUsageDescription</key><string>Butuh lokasi untuk peta</string>

4. TROUBLESHOOTING ERROR UMUM