BAB 10
ADVANCED MOBILE FEATURES
Naik kelas dari aplikasi biasa menjadi aplikasi kelas produksi — mengakses kamera, file, notifikasi, dan dunia di luar app-mu.
Gunakan → / ← untuk navigasi, ☰ untuk daftar slide. Setiap slide bisa di-scroll pada zoom 100%.
Peta Perjalanan BAB 10
10 fitur device-level + 1 mini project. Semua bermuara pada satu aplikasi nyata: Productivity App.
ANALOGI BERGERAKBayangkan kamu pindah dari bengkel rumahan (app sederhana BAB 1–9) ke pabrik perakitan (app produksi). Tukang (🛠️) terus membawa komponen baru — kamera, notifikasi, PDF — dari developer ke produk akhir. Itulah yang kita rakit di bab ini.
🗺️ 10 Stasiun Fitur
1. Camera
Ambil foto langsung dari kamera native device.
2. Gallery
Pilih foto/video dari galeri perangkat.
3. File Picker
Pilih file apa pun dari storage.
4. PDF Viewer
Tampilkan dokumen PDF di dalam app.
5. PDF Generator
Buat PDF dari data app.
6. Share
Bagikan konten ke app lain.
7. Push Notification
Notifikasi dari server (FCM).
8. Local Notification
Notifikasi lokal & terjadwal.
9. Deep Link
Buka halaman spesifik via URL.
10. URL Launcher
Buka browser, email, telepon.
11. Mini Project
Productivity App lengkap.
3 Tugas
Latihan akhir bab.
Setup Project & Izin (Permission)
POINT MATERIFitur device-level butuh izin. Tanpa izin yang dideklarasikan, app akan crash atau diam-diam gagal. Kita pasang dependency & permission lebih dulu.
ANALOGI BERGERAKPermission seperti petugas imigrasi (🛂) di bandara. Sebelum kamu (🧳 app) masuk ke "negara" kamera atau storage, kamu harus menunjukkan visa (izin di manifest). Tidak ada visa → ditolak masuk.
CODE — pubspec.yamldependencies:
flutter:
sdk: flutter
image_picker: ^1.1.2 # camera + gallery
file_picker: ^8.1.2 # pilih file
flutter_pdfview: ^1.3.2 # tampilkan PDF
pdf: ^3.11.1 # generate PDF
printing: ^5.13.2 # print / preview PDF
share_plus: ^10.1.2 # share ke app lain
firebase_messaging: ^15.1.3 # push notification
flutter_local_notifications: ^17.2.3
app_links: ^6.3.2 # deep link
url_launcher: ^6.3.1 # buka URL/telepon/email
path_provider: ^2.1.4 # akses folder lokal
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission
android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"/>
<key>NSCameraUsageDescription</key>
<string>App butuh kamera untuk foto</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>App butuh akses galeri</string>
📷 Camera — Akses Kamera Native
POINT MATERIDengan image_picker, kita memanggil aplikasi kamera bawaan device, lalu menerima kembali file foto untuk ditampilkan/diproses.
ANALOGI BERGERAKBayangkan fotografer studio. Kamu (app) menekan tombol, lampu flash menyala (kilatan), dan fotografer menyerahkan hasil cetak ke tanganmu. App tidak perlu tahu cara kerja lensa — cukup minta dan terima hasil. Itu inti image_picker.
CODE — Flutterimport 'dart:io';
import 'package:image_picker/image_picker.dart';
class CameraService {
final ImagePicker _picker = ImagePicker();
/// Buka kamera, kembalikan File foto (null jika dibatalkan)
Future<File?> takePhoto() async {
final XFile? shot = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 80, // kompres 80%
maxWidth: 1280,
);
if (shot == null) return null; // user batal
return File(shot.path);
}
}
ElevatedButton.icon(
icon: const Icon(Icons.camera_alt),
label: const Text('Ambil Foto'),
onPressed: () async {
final file = await CameraService().takePhoto();
if (file != null) setState(() => _photo = file);
},
);
// menampilkan: _photo != null ? Image.file(_photo!) : Placeholder()
🖼️ Gallery — Pilih dari Galeri
POINT MATERISama seperti kamera, tapi sumbernya ImageSource.gallery. Bisa juga memilih banyak foto sekaligus dengan pickMultiImage().
Galeri adalah album foto keluarga (🗂️). Kamu membuka album, lalu mencabut beberapa lembar foto untuk dibawa. App hanya menerima salinan foto terpilih, bukan seluruh album.
CODE — Flutterfinal ImagePicker _picker = ImagePicker();
// satu foto
Future<File?> pickOne() async {
final x = await _picker.pickImage(source: ImageSource.gallery);
return x == null ? null : File(x.path);
}
// banyak foto sekaligus
Future<List<File>> pickMany() async {
final list = await _picker.pickMultiImage(imageQuality: 80);
return list.map((x) => File(x.path)).toList();
}
source.📁 File Picker — Pilih File Apa Pun
POINT MATERIPaket file_picker membuka file manager device untuk memilih PDF, dokumen, audio, atau tipe khusus (filter berdasarkan ekstensi).
ANALOGI BERGERAKBayangkan lemari arsip kantor (🗄️) berisi ribuan dokumen. File picker adalah asisten yang membuka lemari, kamu sebutkan "ambil yang PDF saja", dan ia mengambilkan dokumen sesuai filter.
CODE — Flutterimport 'package:file_picker/file_picker.dart';
Future<PlatformFile?> pickDocument() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf', 'docx', 'xlsx'],
allowMultiple: false,
);
if (result == null) return null; // dibatalkan
final file = result.files.single;
print('Nama: ${file.name}, ukuran: ${file.size} bytes');
return file; // file.path -> lokasi file
}
240 KB · PDF
withData: true bila ingin membaca byte file langsung tanpa path (berguna untuk upload).📄 PDF Viewer — Tampilkan Dokumen
POINT MATERIflutter_pdfview me-render PDF dari path lokal langsung di dalam app, lengkap dengan navigasi halaman.
ANALOGI BERGERAKPDF Viewer adalah meja baca perpustakaan. Buku (file PDF) dibuka di hadapanmu, kamu bisa membalik halaman, zoom, tapi tidak bisa mengubah isinya — hanya membaca.
CODE — Flutterimport 'package:flutter_pdfview/flutter_pdfview.dart';
class PdfViewPage extends StatelessWidget {
final String path; // path file PDF lokal
const PdfViewPage({super.key, required this.path});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Baca PDF')),
body: PDFView(
filePath: path,
swipeHorizontal: true,
onPageChanged: (page, total) =>
debugPrint('Halaman ${page! + 1} dari $total'),
),
);
}
}
▪ Tugas selesai: 24
▪ Tugas tertunda: 3
▪ Fokus harian: 4.2 jam
◀ Halaman 1 / 3 ▶
🖨️ PDF Generator — Buat Dokumen
POINT MATERIPaket pdf + printing membangun dokumen PDF dari data app (teks, tabel, gambar) lalu menyimpan/print/preview.
ANALOGI BERGERAKIni kebalikan dari Viewer. Di sini app adalah mesin percetakan. Data mentah (⌨️) masuk, dirakit menjadi lembar dokumen (📄), lalu keluar sebagai hasil cetak (🖨️) yang siap dibagikan.
CODE — Flutterimport 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
Future<void> buildReport(List<String> tasks) async {
final doc = pw.Document();
doc.addPage(pw.Page(
pageFormat: PdfPageFormat.a4,
build: (ctx) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Laporan Tugas',
style: pw.TextStyle(fontSize: 24, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(height: 16),
...tasks.map((t) => pw.Bullet(text: t)),
],
),
));
// preview + opsi print/share bawaan OS
await Printing.layoutPdf(onLayout: (format) async => doc.save());
}
Printing.layoutPdf otomatis memunculkan dialog print/share OS — tidak perlu UI sendiri.📤 Share — Berbagi ke App Lain
POINT MATERIshare_plus memunculkan share sheet OS untuk membagikan teks, link, atau file ke WhatsApp, Email, dan app lain.
ANALOGI BERGERAKShare seperti kantor pos / kurir (📤). Kamu menyerahkan paket (konten), kurir menanyakan "mau dikirim ke mana?" lalu mengantarnya ke tujuan (WhatsApp 💬, Email 📧). App-mu tidak perlu tahu detail tiap tujuan.
CODE — Flutterimport 'package:share_plus/share_plus.dart';
// berbagi teks / link
Future<void> shareText(String msg) =>
Share.share(msg, subject: 'Dari Productivity App');
// berbagi file (mis. PDF hasil generate)
Future<void> shareFile(String path) async {
await Share.shareXFiles(
[XFile(path)],
text: 'Ini laporan tugasku 📄',
);
}
Bagikan via…
🔔 Push Notification (FCM)
POINT MATERINotifikasi push dikirim dari server (Firebase Cloud Messaging) ke device, bahkan saat app tertutup. Setiap device punya FCM token sebagai alamat.
ANALOGI BERGERAKServer (☁️) seperti kantor pusat yang mengirim surat (✉️) ke alamat rumahmu (FCM token). Begitu surat tiba, bel pintu berbunyi (🔔) — itulah notifikasi yang muncul di layar, walau kamu sedang tidak membuka app.
CODE — alur data FCMimport 'package:firebase_messaging/firebase_messaging.dart';
class PushService {
final _fcm = FirebaseMessaging.instance;
Future<void> init() async {
// 1. minta izin (Android 13+ / iOS)
await _fcm.requestPermission();
// 2. ambil token device (kirim ke server-mu)
final token = await _fcm.getToken();
print('FCM token: $token');
// 3. pesan saat app dibuka (foreground)
FirebaseMessaging.onMessage.listen((msg) {
print('Pesan masuk: ${msg.notification?.title}');
});
// 4. user tap notifikasi -> app dibuka
FirebaseMessaging.onMessageOpenedApp.listen((msg) {
// navigasi ke halaman terkait
});
}
}
⏰ Local Notification — Terjadwal
POINT MATERIBerbeda dari push, local notification dibuat oleh device itu sendiri — tanpa server. Cocok untuk pengingat & alarm terjadwal.
ANALOGI BERGERAKLocal notification adalah jam weker (⏰) di kamarmu. Kamu menyetelnya sendiri untuk berbunyi jam 7. Tidak ada kantor pos, tidak ada internet — device menyimpan jadwal dan membunyikan bel (🔔) saat waktunya tiba.
CODE — Flutterimport 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
final plugin = FlutterLocalNotificationsPlugin();
Future<void> scheduleReminder(DateTime when, String title) async {
await plugin.zonedSchedule(
0, // id notifikasi
title,
'Saatnya mengerjakan tugasmu!',
tz.TZDateTime.from(when, tz.local),
const NotificationDetails(
android: AndroidNotificationDetails(
'reminder_channel', 'Pengingat Tugas',
importance: Importance.max, priority: Priority.high,
),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
);
}
Saatnya mengerjakan tugasmu!
Sesi fokus 25 menit dimulai.
🔗 Deep Link — Lompat ke Halaman
POINT MATERIDeep link membuka halaman spesifik di dalam app dari URL eksternal, misal myapp://task/42 langsung membuka detail tugas #42.
URL biasa membawamu ke pintu depan gedung (home), lalu kamu harus jalan kaki ke ruangan tujuan. Deep link seperti lift ekspres (🏃) yang langsung mengantarmu ke pintu ruangan spesifik (🚪) — lewati lobi.
CODE — Flutter (app_links)import 'package:app_links/app_links.dart';
final _appLinks = AppLinks();
Future<void> initDeepLinks(GlobalKey<NavigatorState> navKey) async {
// saat app dibuka dari kondisi mati
final initial = await _appLinks.getInitialLink();
if (initial != null) _handle(initial, navKey);
// saat app sedang berjalan
_appLinks.uriLinkStream.listen((uri) => _handle(uri, navKey));
}
void _handle(Uri uri, GlobalKey<NavigatorState> navKey) {
// myapp://task/42 -> pathSegments = ['task','42']
if (uri.pathSegments.first == 'task') {
final id = uri.pathSegments[1];
navKey.currentState?.pushNamed('/task', arguments: id);
}
}
🚀 URL Launcher — Keluar dari App
POINT MATERIurl_launcher membuka resource di luar app: browser (https), email (mailto), telepon (tel), WhatsApp, atau peta.
ANALOGI BERGERAKURL Launcher adalah peluncur roket (🚀). App-mu menyiapkan "tujuan" (https / tel / mailto) lalu menembakkannya ke luar angkasa — sistem operasi menangkap dan membuka aplikasi yang tepat (browser 🌐, dialer 📞).
CODE — Flutterimport 'package:url_launcher/url_launcher.dart';
Future<void> openUrl(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
// contoh pemakaian
openUrl('https://flutter.dev'); // browser
openUrl('tel:+628123456789'); // panggil
openUrl('mailto:guru@sekolah.id'); // email
openUrl('https://wa.me/628123456'); // WhatsApp
<queries> di manifest agar canLaunchUrl mengenali scheme tel/mailto.🛡️ Pola Permission Runtime
POINT MATERIMendeklarasikan izin di manifest belum cukup. Sejak Android 6+/iOS, izin sensitif harus diminta saat runtime dengan UX yang baik.
ANALOGI BERGERAKSeperti masuk ruang server perusahaan: punya kartu akses (manifest) saja tidak cukup, satpam (🛂) tetap meminta konfirmasi langsung di pintu. Kamu jelaskan tujuanmu (rationale), baru pintu kamera (📷) terbuka.
FLOWCHART ANIMASI — State Permissiondenied → tampilkan rationale
permanent → arahkan ke Settings
import 'package:permission_handler/permission_handler.dart';
Future<bool> ensureCamera() async {
final status = await Permission.camera.status;
if (status.isGranted) return true;
if (status.isPermanentlyDenied) {
await openAppSettings(); // arahkan ke pengaturan
return false;
}
final result = await Permission.camera.request();
return result.isGranted;
}
🏆 Productivity App
Satu aplikasi yang menggabungkan semua 10 fitur menjadi produk nyata.
KONSEP APLIKASI📋 Fitur Inti
- Buat & kelola tugas (CRUD)
- Lampirkan foto dari kamera/galeri
- Lampirkan file (PDF/dokumen)
- Reminder via local notification
- Push dari server saat deadline
🚀 Fitur Lanjutan
- Export PDF laporan tugas
- Share laporan ke WhatsApp/email
- Deep link ke detail tugas
- URL launcher ke link referensi
- PDF viewer baca lampiran
Sampai sini kamu sudah punya semua komponen (📷📄🔔📤). Mini project adalah jalur perakitan (🏗️) yang menyatukan komponen-komponen itu menjadi satu kendaraan utuh yang bisa dikendarai pengguna.
🧱 Arsitektur Productivity App
POINT MATERIGunakan Clean Architecture (BAB 6) + Riverpod (BAB 5). Fitur device dibungkus sebagai Service di layer data, dipanggil lewat Provider.
FLOWCHART ANIMASI — Aliran Datalib/
├─ features/task/
│ ├─ presentation/ # TaskPage, TaskDetailPage (deep link target)
│ ├─ application/ # TaskController (Riverpod Notifier)
│ ├─ domain/ # Task entity, TaskRepository (abstract)
│ └─ data/ # TaskRepositoryImpl + local storage
├─ core/services/
│ ├─ camera_service.dart # ① ② image_picker
│ ├─ file_service.dart # ③ file_picker
│ ├─ pdf_service.dart # ④ ⑤ view + generate
│ ├─ share_service.dart # ⑥ share_plus
│ ├─ notification_service.dart# ⑦ ⑧ push + local
│ └─ link_service.dart # ⑨ ⑩ deep link + launcher
└─ main.dart # init Firebase, notif, deep link
⚙️ Kode Inti — Menyatukan Fitur
CODE — entity & controllerclass Task {
final String id, title;
final String? photoPath, filePath;
final DateTime? reminder;
Task({required this.id, required this.title,
this.photoPath, this.filePath, this.reminder});
}
class TaskController extends StateNotifier<List<Task>> {
TaskController(this._cam, this._notif) : super([]);
final CameraService _cam;
final NotificationService _notif;
Future<void> addWithPhoto(String title, DateTime when) async {
final photo = await _cam.takePhoto(); // ①
final task = Task(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title, photoPath: photo?.path, reminder: when);
state = [...state, task];
await _notif.scheduleReminder(when, title); // ⑧
}
}
Future<void> exportAndShare(List<Task> tasks) async {
final path = await PdfService().buildReport(tasks); // ⑤ generate
await ShareService().shareFile(path); // ⑥ share
}
⏰ 07:00 · 📎 foto terlampir
⏰ 13:00 · 📎 laporan.pdf
Selesai 🎉
🧭 Rangkuman BAB 10
Sebelum tugas, pastikan kamu menguasai peta fitur & paket kuncinya.
ANALOGI BERGERAKKamu telah merakit kendaraan produksimu (🏆). Tabel di bawah adalah buku manual-nya — fitur, paket, dan kapan dipakai.
📦 Paket Kunci
- image_picker → kamera & galeri
- file_picker → pilih file
- flutter_pdfview → baca PDF
- pdf + printing → buat PDF
- share_plus → berbagi
📦 Paket Kunci (2)
- firebase_messaging → push
- flutter_local_notifications → reminder
- app_links → deep link
- url_launcher → buka eksternal
- permission_handler → izin runtime
Tugas 1 — Photo Note
ANALOGI BERGERAKBuat aplikasi Photo Note sederhana: ambil foto dari kamera atau galeri, lalu tampilkan foto beserta catatan teks yang diketik pengguna.
🎯 Kriteria Wajib
- Tombol "Kamera" dan "Galeri" memakai image_picker
- Foto terpilih ditampilkan dengan
Image.file - TextField untuk menulis catatan di bawah foto
- Tangani kasus user membatalkan pemilihan (null safety)
- Kompres foto dengan
imageQuality: 70
💡 Lihat Petunjuk
Gunakan satu fungsi _pick(ImageSource source) agar tombol kamera & galeri berbagi kode yang sama. Simpan hasil di File? _image dan panggil setState.
main.dart. Estimasi: 30–45 menit.Tugas 2 — Daily Reminder
ANALOGI BERGERAKBuat aplikasi Daily Reminder: pengguna mengetik judul pengingat & memilih waktu, lalu app menjadwalkan local notification yang berbunyi pada waktu tersebut.
🎯 Kriteria Wajib
- Inisialisasi flutter_local_notifications + timezone
- Minta izin POST_NOTIFICATIONS (Android 13+) saat runtime
showTimePickeruntuk memilih jam- Jadwalkan dengan
zonedSchedule - Daftar pengingat aktif tampil di layar (ListView)
💡 Lihat Petunjuk
Jangan lupa tz.initializeTimeZones() di main(). Gunakan AndroidScheduleMode.exactAllowWhileIdle agar notifikasi tetap akurat saat device idle.
Tugas 3 — Report & Share
ANALOGI BERGERAKGabungkan beberapa fitur: dari daftar tugas, generate PDF laporan, tampilkan di PDF viewer, lalu share file ke aplikasi lain.
ALUR YANG HARUS DIBANGUN (FLOWCHART)🎯 Kriteria Wajib
- Generate PDF berisi judul, tabel tugas, & tanggal (paket pdf)
- Simpan PDF ke folder lokal (path_provider)
- Buka hasil di flutter_pdfview
- Tombol share file PDF dengan share_plus
- Bonus: tombol "Hubungi Guru" pakai url_launcher (mailto)
💡 Lihat Petunjuk
Simpan file via getApplicationDocumentsDirectory(), tulis byte dengan File(path).writeAsBytes(await doc.save()). Path yang sama dipakai untuk viewer & share.