Gallery App dengan Fitur Favorit Zapp.run Flutter
Gallery App dengan Fitur Favorit
📱 Gambaran Umum
Aplikasi Flutter ini adalah contoh implementasi komponen UI/UX modern dengan fitur responsif, manajemen state, dan navigasi multi-halaman. Aplikasi ini menampilkan galeri gambar dengan kemampuan menandai gambar favorit.
🎯 Fitur Utama
1. Antarmuka Responsif
- Beradaptasi otomatis dengan ukuran layar berbeda (mobile & tablet)
- Menggunakan **LayoutBuilder** untuk mendeteksi constraints
- Tampilan berbeda untuk layout sempit (single column) dan lebar (grid view)
2. Manajemen State
- Menggunakan **StatefulWidget** untuk UI yang dinamis
- Implementasi **setState()** untuk update tampilan
- Contoh perbedaan Stateless vs Stateful Widget
3. Navigasi Multi-Halaman
- **BottomNavigationBar** dengan 3 halaman: Home, Favorites, Profile
- **Navigator.push()** dan **Navigator.pop()** untuk navigasi ke halaman detail
- Transisi antar halaman yang smooth
4. Dynamic Theme
- Toggle tema gelap/terang dengan IconButton di AppBar
- Menggunakan **ThemeMode** untuk mengontrol tema aplikasi
- Terintegrasi dengan ThemeData light dan dark
5. Fitur Favorit
- Sistem manajemen favorit dengan class **FavoritesManager**
- Bisa menambah/menghapus gambar dari daftar favorit
- Notifikasi Snackbar saat menambah favorit
- Halaman terpisah untuk melihat daftar favorit
6. Layout Komponen
- Implementasi **Column** dan **Row** untuk tata letak
- Penggunaan **Expanded** dan **Flexible** untuk pembagian ruang
- **GridView** untuk menampilkan gambar dalam grid
🏗️ Struktur Kode
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ThemeMode _themeMode = ThemeMode.light;
void _toggleTheme(bool isDark) {
setState(() {
_themeMode = isDark ? ThemeMode.dark : ThemeMode.light;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Gallery App with Favorites',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: _themeMode,
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
final List<Widget> _pages = [
const HomeContent(),
const FavoritesScreen(),
const ProfileScreen(),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Gallery App'),
actions: [
IconButton(
icon: const Icon(Icons.brightness_6),
onPressed: () {
final myAppState = context.findAncestorStateOfType<_MyAppState>();
if (myAppState != null) {
final isDark = Theme.of(context).brightness == Brightness.dark;
myAppState._toggleTheme(!isDark);
}
},
),
],
),
body: _pages[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.favorite),
label: 'Favorites',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
),
);
}
}
class HomeContent extends StatelessWidget {
const HomeContent({super.key});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Column(
children: [
// Header
Container(
height: constraints.maxHeight * 0.15,
color: Colors.blue,
child: Center(
child: Text(
'Gallery App',
style: TextStyle(
fontSize: constraints.maxWidth > 600 ? 24 : 18,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
// Konten utama dengan Row/Column yang responsif
Expanded(
child: constraints.maxWidth > 600
? _buildWideLayout(context)
: _buildNarrowLayout(context),
),
// Footer
Container(
height: constraints.maxHeight * 0.1,
color: Colors.grey[300],
child: Center(
child: Text(
'Footer Section © 2023',
style: TextStyle(
fontSize: constraints.maxWidth > 600 ? 16 : 12,
),
),
),
),
],
);
},
);
}
Widget _buildNarrowLayout(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 16),
const Text(
'Image Gallery',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
_buildImageCard(
imageUrl: 'https://picsum.photos/300/200?random=1',
title: 'Pemandangan 1',
),
_buildImageCard(
imageUrl: 'https://picsum.photos/300/200?random=2',
title: 'Pemandangan 2',
),
_buildImageCard(
imageUrl: 'https://picsum.photos/300/200?random=3',
title: 'Pemandangan 3',
),
_buildImageCard(
imageUrl: 'https://picsum.photos/300/200?random=4',
title: 'Pemandangan 4',
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailScreen()),
);
},
child: const Text('Lihat Detail'),
),
const SizedBox(height: 16),
],
),
);
}
Widget _buildWideLayout(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
flex: 2,
child: Column(
children: [
const Text(
'Image Gallery',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Expanded(
child: GridView.count(
crossAxisCount: 2,
children: [
_buildImageCard(
imageUrl: 'https://picsum.photos/300/200?random=1',
title: 'Pemandangan 1',
),
_buildImageCard(
imageUrl: 'https://picsum.photos/300/200?random=2',
title: 'Pemandangan 2',
),
_buildImageCard(
imageUrl: 'https://picsum.photos/300/200?random=3',
title: 'Pemandangan 3',
),
_buildImageCard(
imageUrl: 'https://picsum.photos/300/200?random=4',
title: 'Pemandangan 4',
),
],
),
),
],
),
),
const SizedBox(width: 16),
Expanded(
flex: 1,
child: Column(
children: [
const Text(
'Info',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const Text('Ini adalah aplikasi galeri gambar dengan fitur favorit.'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailScreen()),
);
},
child: const Text('Lihat Detail'),
),
],
),
),
],
),
);
}
Widget _buildImageCard({required String imageUrl, required String title}) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
imageUrl,
height: 150,
width: double.infinity,
fit: BoxFit.cover,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.favorite_border),
onPressed: () {
// Akan menambahkan ke favorit
FavoritesManager.addToFavorites(imageUrl, title);
ScaffoldMessenger.of(KeyManager.scaffoldKey.currentContext!).showSnackBar(
SnackBar(content: Text('$title ditambahkan ke favorit')),
);
},
),
],
),
),
],
),
);
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Detail Screen'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.network(
'https://picsum.photos/300/200?random=5',
width: 300,
height: 200,
fit: BoxFit.cover,
),
const SizedBox(height: 20),
const Text(
'Ini adalah halaman detail dengan gambar',
style: TextStyle(fontSize: 20),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Kembali'),
),
],
),
),
);
}
}
class FavoritesScreen extends StatefulWidget {
const FavoritesScreen({super.key});
@override
State<FavoritesScreen> createState() => _FavoritesScreenState();
}
class _FavoritesScreenState extends State<FavoritesScreen> {
List<Map<String, String>> favoriteItems = [];
@override
void initState() {
super.initState();
favoriteItems = FavoritesManager.getFavorites();
}
void _removeFavorite(int index) {
setState(() {
FavoritesManager.removeFromFavorites(favoriteItems[index]['imageUrl']!);
favoriteItems.removeAt(index);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: favoriteItems.isEmpty
? const Center(
child: Text('Belum ada gambar favorit'),
)
: ListView.builder(
itemCount: favoriteItems.length,
itemBuilder: (context, index) {
return ListTile(
leading: Image.network(
favoriteItems[index]['imageUrl']!,
width: 50,
height: 50,
fit: BoxFit.cover,
),
title: Text(favoriteItems[index]['title']!),
trailing: IconButton(
icon: const Icon(Icons.favorite, color: Colors.red),
onPressed: () => _removeFavorite(index),
),
);
},
),
);
}
}
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircleAvatar(
radius: 50,
backgroundImage: NetworkImage('https://via.placeholder.com/100'),
),
const SizedBox(height: 16),
const Text(
'John Doe',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'johndoe@example.com',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
// Aksi untuk mengedit profil
},
child: const Text('Edit Profil'),
),
],
),
);
}
}
class FavoritesManager {
static List<Map<String, String>> _favorites = [];
static void addToFavorites(String imageUrl, String title) {
// Cek apakah sudah ada di favorit
if (!_favorites.any((item) => item['imageUrl'] == imageUrl)) {
_favorites.add({
'imageUrl': imageUrl,
'title': title,
});
}
}
static void removeFromFavorites(String imageUrl) {
_favorites.removeWhere((item) => item['imageUrl'] == imageUrl);
}
static List<Map<String, String>> getFavorites() {
return _favorites;
}
}
class KeyManager {
static final GlobalKey<ScaffoldMessengerState> scaffoldKey = GlobalKey<ScaffoldMessengerState>();
}
Widget Utama:
- **MyApp**: Root widget dengan toggle theme
- **HomeScreen**: Scaffold utama dengan BottomNavigationBar
- **HomeContent**: Halaman beranda dengan galeri gambar
- **FavoritesScreen**: Halaman daftar favorit
- **ProfileScreen**: Halaman profil pengguna
- **DetailScreen**: Halaman detail dengan navigasi push/pop
Kelas Pendukung:
- **FavoritesManager**: Kelas untuk mengelola data favorit
- **KeyManager**: Kelas untuk mengelola global key
💡 Konsep Penting
State Management
- **StatefulWidget** untuk data yang berubah
- **setState()** memicu rebuild widget
- **StatelessWidget** untuk UI statis
Responsive Design
- Media query dengan **LayoutBuilder**
- Adaptasi layout berdasarkan screen size
- Penggunaan constraints.maxWidth untuk conditional rendering
Navigator System
- **Navigator.push()** untuk beralih ke halaman baru
- **Navigator.pop()** untuk kembali ke halaman sebelumnya
- BottomNavigationBar untuk navigasi utama
🎨 UI Components
- **AppBar** dengan actions untuk toggle theme
- **Card** untuk menampilkan gambar dan informasi
- **ListView** dan **GridView** untuk layout yang berbeda
- **CircleAvatar** untuk tampilan profil
- **SnackBar** untuk notifikasi interaksi
📦 Cara Menjalankan
1. Simpan kode dalam file `main.dart`
2. Jalankan dengan `flutter run`
3. Test responsivitas dengan resize emulator/window
4. Test toggle theme dengan ikon brightness di AppBar
5. Test fitur favorit dengan menekan ikon hati pada gambar
Aplikasi ini merupakan contoh lengkap implementasi fitur-fitur fundamental Flutter yang dapat dikembangkan lebih lanjut untuk keperluan production.

Komentar
Posting Komentar