Panduan praktikum step by step yang bisa langsung dipakai di modul atau lembar kerja siswa.
Panduan ini sudah mencakup dua praktikum (Layout Responsif + Navigasi Aplikasi) dalam satu project, lengkap dengan penjelasan setiap bagian kodenya.
🧑💻 Panduan Praktikum Flutter
Praktikum 1 & 2 — Layout Responsif & Navigasi Aplikasi
Target:
Siswa memahami layout responsif menggunakan
Column
,Row
,Flexible
,Expanded
, danLayoutBuilder
.Siswa mampu membuat navigasi antar-halaman menggunakan
Navigator.push()
danBottomNavigationBar
.
📌 Langkah-Langkah Praktikum
1️⃣ Membuat Project Baru
Buka zapp.run.
Pilih template Flutter.
Klik Create → Akan terbuka file
main.dart
.Hapus kode bawaan dan ganti dengan kode berikut.
2️⃣ Salin Kode Berikut
import 'dart:async';import 'package:flutter/material.dart';
void main() { runApp(const WisataApp());}
class WisataApp extends StatefulWidget { const WisataApp({super.key});
@override State<WisataApp> createState() => _WisataAppState();}
class _WisataAppState extends State<WisataApp> { ThemeMode _themeMode = ThemeMode.light;
void _toggleTheme(bool isDark) { setState(() { _themeMode = isDark ? ThemeMode.dark : ThemeMode.light; }); }
@override Widget build(BuildContext context) { return MaterialApp( title: 'Aplikasi Wisata - Demo', debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal), useMaterial3: true, ), darkTheme: ThemeData.dark(useMaterial3: true), themeMode: _themeMode, home: RootPage(onThemeChanged: _toggleTheme, isDark: _themeMode == ThemeMode.dark), ); }}
// Model data destinasi wisataclass Destination { final String title; final String location; final List<String> images; final String description;
Destination( {required this.title, required this.location, required this.images, required this.description});}
final destinations = [ Destination( title: 'Pantai Pasir Putih', location: 'Pesisir Selatan', images: [ 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=1200', 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=1200', 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=1200', ], description: 'Pasir putih lembut, air jernih, sunset menawan.', ), Destination( title: 'Bukit Hijau', location: 'Dataran Tinggi', images: [ 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=1200', 'https://images.unsplash.com/photo-1508672019048-805c876b67e2?w=1200', 'https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=1200', ], description: 'Panorama lembah dan jalur trekking yang menyejukkan.', ),];
class RootPage extends StatefulWidget { final Function(bool) onThemeChanged; final bool isDark; const RootPage({super.key, required this.onThemeChanged, required this.isDark});
@override State<RootPage> createState() => _RootPageState();}
class _RootPageState extends State<RootPage> { int _selectedIndex = 0; final PageController _pageController = PageController(); int _currentCarouselIndex = 0; Timer? _carouselTimer; final List<Destination> _favorites = [];
@override void initState() { super.initState(); _startAutoSlide(); }
void _startAutoSlide() { _carouselTimer = Timer.periodic(const Duration(seconds: 3), (timer) { if (_pageController.hasClients) { setState(() { _currentCarouselIndex = (_currentCarouselIndex + 1) % destinations.length; _pageController.animateToPage( _currentCarouselIndex, duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, ); }); } }); }
void _onItemTapped(int index) { setState(() => _selectedIndex = index); }
void _toggleFavorite(Destination d) { setState(() { if (_favorites.contains(d)) { _favorites.remove(d); } else { _favorites.add(d); } }); }
@override void dispose() { _carouselTimer?.cancel(); _pageController.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { final pages = [ _buildHome(), FavoritesPage(favorites: _favorites, onToggle: _toggleFavorite), ProfilePage( isDark: widget.isDark, onThemeToggle: widget.onThemeChanged, ), ];
return Scaffold( appBar: AppBar( title: const Text('Keindahan Wisata Nusantara'), centerTitle: true, ), body: pages[_selectedIndex], bottomNavigationBar: BottomNavigationBar( currentIndex: _selectedIndex, onTap: _onItemTapped, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem(icon: Icon(Icons.favorite), label: 'Favorit'), BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profil'), ], ), ); }
Widget _buildHome() { return SafeArea( child: LayoutBuilder( builder: (context, constraints) { final wide = constraints.maxWidth > 700; return Column( children: [ SizedBox( height: wide ? 300 : 200, child: PageView.builder( controller: _pageController, itemCount: destinations.length, onPageChanged: (i) => setState(() => _currentCarouselIndex = i), itemBuilder: (context, index) { final d = destinations[index]; return GestureDetector( onTap: () => Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( destination: d, onFav: _toggleFavorite, isFav: _favorites.contains(d)))), child: Stack( fit: StackFit.expand, children: [ Image.network(d.images[0], fit: BoxFit.cover), Container( alignment: Alignment.bottomLeft, padding: const EdgeInsets.all(16), color: Colors.black26, child: Text(d.title, style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)), ) ], ), ); }, ), ), Expanded( child: wide ? Row( children: [ Expanded( child: DestinationsGrid( onFav: _toggleFavorite, favorites: _favorites)), const SizedBox(width: 12), const Expanded(child: PromoPanel()), ], ) : Column( children: [ Expanded( child: DestinationsGrid( onFav: _toggleFavorite, favorites: _favorites)), const PromoPanel(), ], ), ), ], ); }, ), ); }}
class DestinationsGrid extends StatelessWidget { final Function(Destination) onFav; final List<Destination> favorites; const DestinationsGrid( {super.key, required this.onFav, required this.favorites});
@override Widget build(BuildContext context) { return GridView.builder( padding: const EdgeInsets.all(12), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 1.5, crossAxisSpacing: 8, mainAxisSpacing: 8, ), itemCount: destinations.length, itemBuilder: (context, index) { final d = destinations[index]; final isFav = favorites.contains(d); return GestureDetector( onTap: () => Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage(destination: d, onFav: onFav, isFav: isFav))), child: Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Column( children: [ Expanded( child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: Stack( children: [ Image.network(d.images[1], fit: BoxFit.cover, width: double.infinity), Positioned( top: 8, right: 8, child: IconButton( icon: Icon( isFav ? Icons.favorite : Icons.favorite_border, color: Colors.red), onPressed: () => onFav(d), ), ), ], ), ), ), Padding( padding: const EdgeInsets.all(8.0), child: Text(d.title, style: const TextStyle(fontWeight: FontWeight.bold)), ) ], ), ), ); }, ); }}
class PromoPanel extends StatelessWidget { const PromoPanel({super.key});
@override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text('Promo & Info', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 8), Text('• Diskon tiket akhir pekan'), Text('• Paket edukasi study tour'), ], ), ), ); }}
class DetailPage extends StatelessWidget { final Destination destination; final Function(Destination) onFav; final bool isFav; const DetailPage( {super.key, required this.destination, required this.onFav, required this.isFav});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(destination.title), actions: [ IconButton( icon: Icon(isFav ? Icons.favorite : Icons.favorite_border), onPressed: () => onFav(destination), ) ], ), body: ListView( padding: const EdgeInsets.all(12), children: [ SizedBox( height: 250, child: PageView( children: destination.images .map((img) => ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.network(img, fit: BoxFit.cover))) .toList(), ), ), const SizedBox(height: 12), Text(destination.location, style: const TextStyle(fontWeight: FontWeight.w600)), const SizedBox(height: 8), Text(destination.description), ], ), ); }}
class FavoritesPage extends StatelessWidget { final List<Destination> favorites; final Function(Destination) onToggle; const FavoritesPage( {super.key, required this.favorites, required this.onToggle});
@override Widget build(BuildContext context) { if (favorites.isEmpty) { return const Center(child: Text('Belum ada destinasi favorit')); } return ListView.builder( padding: const EdgeInsets.all(12), itemCount: favorites.length, itemBuilder: (context, index) { final d = favorites[index]; return Card( child: ListTile( leading: ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network(d.images[0], width: 60, fit: BoxFit.cover)), title: Text(d.title), subtitle: Text(d.location), trailing: IconButton( icon: const Icon(Icons.remove_circle), onPressed: () => onToggle(d)), onTap: () => Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage(destination: d, onFav: onToggle, isFav: true))), ), ); }, ); }}
class ProfilePage extends StatelessWidget { final bool isDark; final Function(bool) onThemeToggle;
const ProfilePage( {super.key, required this.isDark, required this.onThemeToggle});
@override Widget build(BuildContext context) { return ListView( padding: const EdgeInsets.all(20), children: [ const CircleAvatar( radius: 50, backgroundImage: NetworkImage( 'https://i.pravatar.cc/150?img=3'), // avatar dummy ), const SizedBox(height: 12), const Center( child: Text('Budi Santoso', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold))), const Center(child: Text('budi.santoso@example.com')), const Divider(height: 40), SwitchListTile( title: const Text('Mode Gelap'), value: isDark, onChanged: onThemeToggle, secondary: const Icon(Icons.dark_mode), ), const ListTile( leading: Icon(Icons.settings), title: Text('Pengaturan Akun'), ), const ListTile( leading: Icon(Icons.logout), title: Text('Keluar'), ), ], ); }}
3️⃣ Jalankan Program
Klik tombol ▶ Run di zapp.run.
Amati tampilan layout
Klik tombol "Home" atau ikon Favorit → akan pindah ke halaman sesuai menu.
📑 Tabel Penjelasan Kode – Aplikasi Wisata Flutter
Bagian Kode | Penjelasan | Konsep Flutter |
---|---|---|
runApp(const WisataApp()); |
Menjalankan widget utama WisataApp . |
Entry Point aplikasi Flutter. |
class WisataApp extends StatelessWidget |
Widget utama yang mengatur tema aplikasi dan halaman awal (home: RootPage() ). |
StatelessWidget – tidak menyimpan state, hanya membangun UI. |
Destination class |
Model data untuk destinasi wisata (judul, lokasi, gambar, deskripsi). | Model Data untuk memudahkan pengelolaan informasi. |
sampleDestinations |
List berisi contoh data destinasi wisata. | List & Object di Dart. |
class RootPage extends StatefulWidget |
Halaman utama yang menyimpan state (index navigasi, controller karosel). | StatefulWidget – dapat berubah sesuai interaksi user. |
_selectedIndex |
Menyimpan halaman aktif (Home, Favorites, Profile). | State Management sederhana. |
_carouselController |
Mengontrol pergerakan PageView pada karosel. |
PageController – mengatur slide. |
_buildHomeContent() |
Menyusun tampilan halaman Home: sidebar, karosel, grid destinasi, panel promo. | Column, Row, Expanded, LayoutBuilder untuk layout responsif. |
PageView.builder(...) |
Menampilkan karosel gambar destinasi. | PageView – membuat slide horizontal. |
Expanded(...) |
Membuat widget mengambil sisa ruang yang tersedia. | Expanded – konsep layout fleksibel. |
LayoutBuilder(...) |
Mengecek lebar layar (constraints.maxWidth ) untuk menentukan apakah layout lebar atau sempit. |
Responsif – menyesuaikan layout untuk desktop/tablet/HP. |
_buildFavoritesPage() |
Menampilkan placeholder jika belum ada destinasi favorit. | Column + Icon + Text dalam Center. |
_buildProfilePage() |
Menampilkan informasi pengguna dan deskripsi aplikasi. | SingleChildScrollView agar halaman dapat digeser ke bawah. |
BottomNavigationBar(...) |
Navigasi bawah untuk berpindah antara Home, Favorites, Profile. | BottomNavigationBar + setState() untuk mengubah halaman. |
SideBar & AppDrawer |
Sidebar untuk layar lebar, Drawer untuk layar kecil. | Drawer – menu geser dari samping. |
DestinationsGrid(...) |
Menampilkan daftar destinasi dalam bentuk grid responsif. | GridView.builder – list grid dengan jumlah kolom dinamis. |
DestinationCard(...) |
Kartu destinasi: gambar kiri + teks kanan + tombol. | Card + Row + Flexible untuk layout responsif. |
DetailPage(...) |
Halaman detail destinasi, menampilkan karosel gambar & deskripsi lengkap. | Navigator.push() – pindah halaman. |
DetailImageCarousel(...) |
Karosel di halaman detail dengan dot indicator. | StatefulWidget + PageView untuk menampilkan slide dengan indikator aktif. |
SafeArea(...) |
Melindungi konten dari area tertutup (notch, status bar, bottom nav). | SafeArea – membuat UI aman dari overlap. |
🎯 Poin Pembelajaran untuk Siswa
-
Layout Responsif: Gunakan Column
, Row
, Expanded
, Flexible
, dan LayoutBuilder
untuk menyesuaikan tampilan di berbagai ukuran layar.
-
Navigasi: Gunakan Navigator.push()
untuk pindah ke halaman baru (misal DetailPage) dan BottomNavigationBar
untuk navigasi antar-halaman utama.
-
State Management: Gunakan StatefulWidget
dan setState()
untuk mengubah tampilan saat user berinteraksi (misal berpindah tab).
-
Komponen UI: Kenali penggunaan Card
, GridView
, Drawer
, dan PageView
untuk membuat aplikasi menarik dan interaktif.
Layout Responsif: Gunakan Column
, Row
, Expanded
, Flexible
, dan LayoutBuilder
untuk menyesuaikan tampilan di berbagai ukuran layar.
Navigasi: Gunakan Navigator.push()
untuk pindah ke halaman baru (misal DetailPage) dan BottomNavigationBar
untuk navigasi antar-halaman utama.
State Management: Gunakan StatefulWidget
dan setState()
untuk mengubah tampilan saat user berinteraksi (misal berpindah tab).
Komponen UI: Kenali penggunaan Card
, GridView
, Drawer
, dan PageView
untuk membuat aplikasi menarik dan interaktif.
🎯 Tugas Siswa
- Tambahkan 1 destinasi baru dengan gambar dan deskripsi sendiri.
- Ubah durasi auto-slide carousel menjadi 5 detik.
- Buat halaman profil dengan nama & foto kalian.
💡 Tips Guru
Ajak siswa membandingkan hasil di layar HP dan di layar PC untuk memahami konsep responsif.
Diskusikan kapan sebaiknya menggunakan Column + Row dibanding GridView.
Tantang siswa untuk membuat tema warna sendiri menggunakan
ThemeData
.
Salam : Redaksi
Tidak ada komentar:
Posting Komentar