Membangun Pondasi Aplikasi Flutter Skala Industri
Tujuan Pembelajaran
Setelah mengikuti pertemuan ini, siswa mampu:
1. Membuat project Flutter dengan struktur folder yang rapi
2. Menampilkan data produk dalam bentuk grid modern
3. Menggunakan gambar, warna, dan layout yang lebih profesional
4. Membuat aplikasi multi-halaman (Home, Detail, Profile)
5. Menyiapkan project agar siap masuk ke Provider, API, dan Firebase
Pertanyaan Pemantik
Pernahkah kalian melihat aplikasi yang awalnya lancar, tapi semakin lama semakin sulit dikembangkan?
Menurut kalian, apa yang terjadi jika sebuah aplikasi besar dibuat tanpa perencanaan sejak awal?
Diskusikan singkat (3–5 menit) dengan teman sebangku.
1. Aplikasi Latihan vs Aplikasi Dunia Kerja
Bayangkan:
- Aplikasi latihan itu seperti rumah kardus → cepat dibuat, tapi tidak kuat kalau mau ditambah “lantai”.
- Aplikasi dunia kerja itu seperti rumah beton → butuh pondasi kuat agar bisa ditambah fitur kapan saja.
Di semester 1 kita banyak membangun “rumah kardus” (UI, widget, state sederhana).
Di semester 2 kita mulai membangun “rumah beton”:
- kode rapi
- folder jelas
- mudah dikembangkan (dan mudah dibaca orang lain)
2. Kenapa Struktur Folder Itu Penting?
Perumpamaan:
- kalau semua barang di rumah ditumpuk jadi satu ruangan, lama-lama bingung cari barang.
- kalau tiap barang ada tempatnya (dapur, kamar, gudang), kita cepat menemukan dan lebih rapi.
Di project Flutter:
- kalau semua file ditaruh di main.dart, akan cepat “pusing” saat fitur bertambah.
- kalau folder rapi, nambah fitur jadi lebih mudah (Provider/API/Firebase nanti “nyangkutnya” jelas).
Persiapan
Pastikan kalian sudah:
- Flutter SDK ter-install
- Android Studio / VS Code siap
- flutter doctor tidak error besar
1. Buat Project Baru
Nama project (wajib sama): smk_product_app
Buat lewat terminal:
flutter create smk_product_app
cd smk_product_app
Jalankan untuk memastikan project normal:
flutter run
Kalau sudah tampil aplikasi counter default, lanjut langkah berikutnya.
2. Buat Struktur Folder di lib/
Buat folder berikut:
lib/
├── pages/
├── widgets/
├── models/
├── services/
└── main.dart
Tips cepat (opsional, terminal):
mkdir -p lib/pages lib/widgets lib/models lib/services
3. Buat File & Kode (Wajib)
Kita akan membuat aplikasi sederhana: - Home menampilkan list produk - Klik produk → pindah ke Detail
A. Buat Model Data
File: lib/models/product.dart
class Product {
final String id;
final String name;
final String category;
final int price; // Rupiah
final String imageUrl; // Network image (sementara)
final String description;
final double rating; // 0.0 - 5.0
const Product({
required this.id,
required this.name,
required this.category,
required this.price,
required this.imageUrl,
required this.description,
required this.rating,
});
}
B. Buat Dummy Data (sementara di Home)
Nanti saat belajar API/Firebase, dummy data ini akan diganti data online.
C. Buat Widget Reusable untuk Card Produk
File: lib/widgets/product_card.dart
import 'package:flutter/material.dart';
import '../models/product.dart';
// Catatan untuk siswa:
// Format harga akan dibuat reusable di tugas (price_text.dart).
// Untuk pertemuan ini, tampilkan harga dengan format sederhana terlebih dahulu.
class ProductCard extends StatelessWidget {
final Product product;
final VoidCallback onTap;
const ProductCard({
super.key,
required this.product,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: theme.colorScheme.surface,
boxShadow: [
BoxShadow(
blurRadius: 14,
offset: const Offset(0, 8),
color: Colors.black.withValues(alpha: 0.08),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
child: AspectRatio(
aspectRatio: 4 / 3,
child: Stack(
fit: StackFit.expand,
children: [
Hero(
tag: "product-image-${product.id}",
child: Image.network(
product.imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, progress) {
if (progress == null) return child;
return Container(
color: theme.colorScheme.surfaceContainerHighest,
child: const Center(child: CircularProgressIndicator()),
);
},
errorBuilder: (_, __, ___) {
return Container(
color: theme.colorScheme.surfaceContainerHighest,
child: const Center(
child: Icon(Icons.broken_image_outlined, size: 40),
),
);
},
),
),
Positioned(
left: 10,
top: 10,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.55),
borderRadius: BorderRadius.circular(999),
),
child: Text(
product.category,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
),
],
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 6),
Text(
product.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
height: 1.2,
),
),
const Spacer(),
Row(
children: [
Expanded(
child: Text(
"Rp ${product.price}",
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w900,
color: theme.colorScheme.primary,
),
),
),
const Icon(Icons.star_rounded, size: 18, color: Colors.amber),
const SizedBox(width: 4),
Text(
product.rating.toStringAsFixed(1),
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
],
),
],
),
),
),
],
),
),
);
}
}
D. Buat Halaman Home (List Produk)
File: lib/pages/home_page.dart
import 'package:flutter/material.dart';
import '../models/product.dart';
import '../widgets/product_card.dart';
import 'detail_page.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final TextEditingController _searchC = TextEditingController();
String _query = "";
List<Product> _dummyProducts() {
// Catatan:
// Gambar masih dummy (picsum). Pada tugas, siswa wajib mengganti gambar
// agar sesuai dengan masing-masing produk.
return const [
Product(
id: "p1",
name: "Keyboard Mechanical Aurora",
category: "Aksesoris",
price: 349000,
imageUrl: "https://picsum.photos/seed/keyboard/800/600",
rating: 4.6,
description:
"Keyboard mechanical dengan switch empuk, backlight RGB, dan build quality kokoh. Cocok untuk coding dan gaming, nyaman dipakai lama.",
),
Product(
id: "p2",
name: "Mouse Wireless Pro",
category: "Aksesoris",
price: 179000,
imageUrl: "https://picsum.photos/seed/mouse/800/600",
rating: 4.4,
description:
"Mouse wireless responsif dengan baterai awet, nyaman digenggam, dan sensor stabil. Pas untuk tugas sekolah dan editing ringan.",
),
Product(
id: "p3",
name: "Headset Studio Mic",
category: "Audio",
price: 399000,
imageUrl: "https://picsum.photos/seed/headset/800/600",
rating: 4.5,
description:
"Headset dengan mikrofon jernih untuk meeting dan presentasi. Suara detail, bantalan nyaman untuk pemakaian panjang.",
),
Product(
id: "p4",
name: "Flashdisk 64GB Speed",
category: "Storage",
price: 99000,
imageUrl: "https://picsum.photos/seed/flashdisk/800/600",
rating: 4.2,
description:
"Flashdisk 64GB dengan transfer cepat untuk tugas sekolah, file project, dan backup. Ringkas dan mudah dibawa.",
),
Product(
id: "p5",
name: "Monitor 24\" IPS Coding",
category: "Komputer",
price: 1599000,
imageUrl: "https://picsum.photos/seed/monitor/800/600",
rating: 4.7,
description:
"Monitor IPS 24 inci dengan warna akurat dan sudut pandang luas. Nyaman untuk coding dan belajar berjam-jam.",
),
Product(
id: "p6",
name: "Laptop Stand Aluminum",
category: "Komputer",
price: 129000,
imageUrl: "https://picsum.photos/seed/stand/800/600",
rating: 4.3,
description:
"Stand laptop berbahan aluminum yang kuat dan stabil. Membantu posisi layar sejajar mata, membuat duduk lebih nyaman.",
),
];
}
@override
void dispose() {
_searchC.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final products = _dummyProducts();
final filtered = products.where((p) {
final q = _query.trim().toLowerCase();
if (q.isEmpty) return true;
return p.name.toLowerCase().contains(q) ||
p.category.toLowerCase().contains(q) ||
p.description.toLowerCase().contains(q);
}).toList();
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
expandedHeight: 160,
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
title: const Text("SMK Product App"),
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
theme.colorScheme.primary,
theme.colorScheme.tertiary,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 56, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Katalog Produk",
style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 6),
Text(
"Cari produk dan lihat detailnya. Ini pondasi untuk Provider, API, dan Firebase.",
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onPrimary.withValues(alpha: 0.9),
),
),
],
),
),
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 10),
child: TextField(
controller: _searchC,
onChanged: (v) => setState(() => _query = v),
decoration: InputDecoration(
hintText: "Cari nama, kategori, atau deskripsi",
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: theme.colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
),
),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 16),
sliver: SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, index) {
final item = filtered[index];
return ProductCard(
product: item,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(product: item),
),
);
},
);
},
childCount: filtered.length,
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 14,
crossAxisSpacing: 14,
childAspectRatio: 0.74,
),
),
),
],
),
);
}
}
Catatan: hari ini kita masih pakai
Navigator.push.
Di Pertemuan 2, ini akan kita upgrade ke go_router agar siap untuk Auth Guard & routing profesional.
E. Buat Halaman Detail
File: lib/pages/detail_page.dart
import 'package:flutter/material.dart';
import '../models/product.dart';
class DetailPage extends StatelessWidget {
final Product product;
const DetailPage({
super.key,
required this.product,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
expandedHeight: 280,
title: Text(product.name, maxLines: 1, overflow: TextOverflow.ellipsis),
flexibleSpace: FlexibleSpaceBar(
background: Stack(
fit: StackFit.expand,
children: [
Hero(
tag: "product-image-${product.id}",
child: Image.network(
product.imageUrl,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
color: theme.colorScheme.surfaceContainerHighest,
child: const Center(
child: Icon(Icons.broken_image_outlined, size: 50),
),
),
),
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.black.withValues(alpha: 0.55),
Colors.transparent,
Colors.black.withValues(alpha: 0.65),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
],
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 26),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
product.name,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(999),
),
child: Text(
product.category,
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w800,
color: theme.colorScheme.onSecondaryContainer,
),
),
),
],
),
const SizedBox(height: 10),
Row(
children: [
Text(
"Rp ${product.price}",
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w900,
color: theme.colorScheme.primary,
),
),
const SizedBox(width: 12),
const Icon(Icons.star_rounded, color: Colors.amber),
const SizedBox(width: 4),
Text(
"${product.rating.toStringAsFixed(1)} / 5.0",
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Icon(Icons.local_shipping_outlined, color: theme.colorScheme.primary),
const SizedBox(width: 10),
Expanded(
child: Text(
"Gratis konsultasi spesifikasi untuk kebutuhan belajar, coding, dan project sekolah.",
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
),
const SizedBox(height: 18),
Text(
"Deskripsi",
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 8),
Text(
product.description,
style: theme.textTheme.bodyMedium?.copyWith(height: 1.4),
),
const SizedBox(height: 22),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Simulasi: ditambahkan ke keranjang")),
);
},
icon: const Icon(Icons.add_shopping_cart_outlined),
label: const Text("Tambah ke Keranjang"),
),
),
],
),
),
),
],
),
);
}
}
F. Update main.dart
File: lib/main.dart
import 'package:flutter/material.dart';
import 'pages/home_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6D28D9),
brightness: Brightness.light,
);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'SMK Product App',
theme: ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
appBarTheme: AppBarTheme(
centerTitle: false,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
),
cardTheme: const CardThemeData(
elevation: 0,
margin: EdgeInsets.zero,
),
),
home: const HomePage(),
);
}
}
Checklist Hasil Akhir Pertemuan 1
Kalian dianggap berhasil jika:
- Project bernama smk_product_app
- Folder pages/widgets/models sudah ada
- Home menampilkan produk dalam bentuk grid
- Klik produk masuk ke Detail (dengan Hero animation gambar)
- UI terlihat rapi dan tidak overflow
Studi Kasus (Setelah Teori)
Kasus: Aplikasi Manajemen Produk untuk Toko
Toko ingin aplikasi yang:
- menampilkan daftar produk
- buka detail produk
- nanti akan ditambah login, API, database
Diskusi (5–10 menit):
1. Kalau semua kode ditaruh di main.dart, apa dampaknya saat fitur bertambah?
2. Dari struktur folder yang kita buat, file mana yang nanti cocok untuk:
- API call?
- login?
- state management?
Tugas
Tugas Level Dasar
- Tambahkan minimal 4 produk lagi (total minimal 10)
- Pastikan deskripsi tidak terlalu pendek (minimal 1–2 kalimat)
- Pastikan grid tetap rapi dan tidak overflow
- Ganti gambarnya produknya menjadi gambar asli
Tugas Level Industri (Lebih Sulit)
- Buat halaman baru
lib/pages/profile_page.dart(isi: nama siswa, kelas, target PKL) - Tambahkan tombol di Home (ikon di AppBar) untuk membuka Profile
- Buat widget reusable
lib/widgets/price_text.dartuntuk format harga (contoh: “Rp 1.500.000”) - Pastikan tidak ada logic format harga yang ditulis ulang di banyak tempat (harus reusable)
- Karena gambar masih dummy, ganti gambar produk dengan gambar yang sesuai dengan masing-masing produk (keyboard, mouse, monitor, headset, dan sebagainya)
Catatan:
- Pada bagian tugas, modul tidak menyediakan contoh kode untuk Profile dan price_text.dart.
- Siswa diminta membaca struktur project dan menerapkan konsep yang sudah dipelajari.
Refleksi Pembelajaran
Tulis jawaban singkat: 1. Bagian mana yang paling menantang saat membuat UI grid? 2. Apa 1 hal yang membuat project ini terlihat lebih “profesional” dibanding semester 1? 3. Menurutmu, kenapa komponen reusable penting sejak awal?
Penutup
Hari ini fokus kita adalah pondasi project dan tampilan yang layak dilihat user.
Pada pertemuan berikutnya, navigasi akan di-upgrade ke routing modern (go_router) agar siap untuk alur login, API, dan Firebase.