Lewati ke isi

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

  1. Tambahkan minimal 4 produk lagi (total minimal 10)
  2. Pastikan deskripsi tidak terlalu pendek (minimal 1–2 kalimat)
  3. Pastikan grid tetap rapi dan tidak overflow
  4. Ganti gambarnya produknya menjadi gambar asli

Tugas Level Industri (Lebih Sulit)

  1. Buat halaman baru lib/pages/profile_page.dart (isi: nama siswa, kelas, target PKL)
  2. Tambahkan tombol di Home (ikon di AppBar) untuk membuka Profile
  3. Buat widget reusable lib/widgets/price_text.dart untuk format harga (contoh: “Rp 1.500.000”)
  4. Pastikan tidak ada logic format harga yang ditulis ulang di banyak tempat (harus reusable)
  5. 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.