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).
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.
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.
$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.
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.
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.
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
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).
// 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
- Error Cleartext HTTP traffic: Android memblokir HTTP biasa. Tambah
android:usesCleartextTraffic="true" di AndroidManifest.xml (di dalam tag <application>).
- Error RenderFlex overflowed: Tampilan "bocor" (Kuning-Hitam). Bungkus Column atau Row dengan
SingleChildScrollView.
- Gambar Silang (Grey Box): Cek URL. Jangan pakai localhost kalau di emulator, pakai
10.0.2.2.