Bikin Aplikasi Jastip Kantin SMKN 1

 Bikin Aplikasi Jastip Kantin SMKN 1 Banjar Pakai Flutter & Provider – Gak Perlu Antre Lagi!

Pernah gak sih kamu ngerasa malas antre di kantin sekolah pas jam istirahat? Baru saja sampai di depan stan makanan, tiba-tiba bel masuk sudah berbunyi. Masalah klasik anak sekolah ini sebenarnya bisa diselesaikan dengan teknologi yang sederhana tapi tepat guna.

Berangkat dari keresahan tersebut, saya mencoba membuat sebuah konsep Aplikasi Jastip (Jasa Titip) & Transaksi Kantin SMKN 1 Banjar berbasis Flutter. Aplikasi ini memungkinkan siswa untuk memesan makanan dari kantin mana pun di sekolah, lalu meneruskan pesanan tersebut ke kurir internal (anak jastip) via WhatsApp untuk diantarkan langsung ke kelas.

Di artikel ini, saya akan membagikan kode lengkapnya yang sudah menggunakan Provider untuk manajemen state dan url_launcher untuk integrasi ke WhatsApp.

πŸš€ Fitur Utama Aplikasi

Daftar Kantin Dinamis: Menampilkan daftar kantin yang ada di SMKN 1 Banjar lengkap dengan nomor WhatsApp tujuan masing-masing.

Manajemen Keranjang Belanja: Menggunakan State Management Provider sehingga tambah-kurang item terasa mulus (reactive).

Pesan Otomatis via WhatsApp: Mengonversi item di keranjang menjadi format teks rapi, lalu membuka WhatsApp Kurir secara otomatis.

πŸ› ️ Tech Stack yang Digunakan

Flutter Core & Material 3 (Untuk UI yang modern dan bersih).

Provider (Untuk mengelola status keranjang belanja secara efisien).

Url Launcher (Untuk menjembatani aplikasi dengan WhatsApp lewat skema Deep Linking).

πŸ“ Struktur Kode & Penjelasan

Aplikasi ini dibangun dengan struktur yang ringkas namun tetap menerapkan best practice. Mari kita bedah bagian-bagian pentingnya:

1. Data Model & State Management (CartManager)

Kita memisahkan model data untuk Canteen dan MenuItem. Bagian paling krusial ada pada CartManager yang bertindak sebagai otak dari keranjang belanja:

class CartManager extends ChangeNotifier {

  final Map<MenuItem, int> _items = {};

  Map<MenuItem, int> get items => Map.unmodifiable(_items);


  void addItem(MenuItem item) {

    _items[item] = (_items[item] ?? 0) + 1;

    notifyListeners(); // Memberitahu UI untuk memperbarui tampilan

  }

  // ... fungsi untuk remove dan hitung total

 harga

}

2. Integrasi WhatsApp Launcher

Ketika tombol "Pesan" ditekan, aplikasi akan mengonversi isi keranjang menggunakan StringBuffer dan melakukan URL Encoding agar teks bisa dikirim dengan aman lewat tautan wa.me.


final url = Uri.parse('https://wa.me/${canteen.waNumber}?text=${Uri.encodeComponent(buffer.toString())}');

if (await canLaunchUrl(url)) {

  await launchUrl(url, mode: LaunchMode.externalApplication);


}


πŸ’» Kode Lengkap Aplikasi (main.dart)

Berikut adalah kode utuh yang bisa kamu coba pasang di proyek Flutter-mu:


import 'package:flutter/material.dart';

import 'package:provider/provider.dart';

import 'package:url_launcher/url_launcher.dart';


void main() {

  runApp(

    ChangeNotifierProvider(

      create: (context) => CartManager(),

      child: const KantinSmkn1BanjarApp(),

    ),

  );

}


class KantinSmkn1BanjarApp extends StatelessWidget {

  const KantinSmkn1BanjarApp({super.key});


  @override

  Widget build(BuildContext context) {

    return MaterialApp(

      title: 'Kantin SMKN 1 Banjar',

      theme: ThemeData(

        colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),

        useMaterial3: true,

      ),

      home: const MainMenuScreen(),

      debugShowCheckedModeBanner: false,

    );

  }

}


// ================= DATA MODELS =================


class Canteen {

  final String id;

  final String name;

  final String icon;

  final String waNumber;


  const Canteen({required this.id, required this.name, required this.icon, required this.waNumber});

}


class MenuItem {

  final String id;

  final String canteenId;

  final String name;

  final int price;


  const MenuItem({

    required this.id,

    required this.canteenId,

    required this.name,

    required this.price,

  });

}


const List<Canteen> listKantin = [

  Canteen(id: 'k1', name: 'Kantin Bu Endang', icon: '🍲', waNumber: '6281288314442'),

  Canteen(id: 'k2', name: 'Kantin Pak Joko', icon: 'πŸ›', waNumber: '6281288314442'),

  Canteen(id: 'k3', name: 'Kantin Mbak Sri', icon: '🍜', waNumber: '6281288314442'),

];


const List<MenuItem> listMenu = [

  MenuItem(id: 'm1', canteenId: 'k1', name: 'Bakso Sapi', price: 12000),

  MenuItem(id: 'm2', canteenId: 'k1', name: 'Soto Ayam', price: 10000),

  MenuItem(id: 'm3', canteenId: 'k2', name: 'Nasi Rames', price: 12000),

  MenuItem(id: 'm4', canteenId: 'k2', name: 'Gorengan', price: 1000),

  MenuItem(id: 'm5', canteenId: 'k3', name: 'Mie Ayam', price: 11000),

  MenuItem(id: 'm6', canteenId: 'k3', name: 'Es Teh Manis', price: 3000),

];


// ================= STATE MANAGEMENT =================


class CartManager extends ChangeNotifier {

  final Map<MenuItem, int> _items = {};


  Map<MenuItem, int> get items => Map.unmodifiable(_items);


  void addItem(MenuItem item) {

    _items[item] = (_items[item] ?? 0) + 1;

    notifyListeners();

  }


  void removeItem(MenuItem item) {

    if (_items.containsKey(item)) {

      if (_items[item]! > 1) {

        _items[item] = _items[item]! - 1;

      } else {

        _items.remove(item);

      }

      notifyListeners();

    }

  }


  int get totalHarga => _items.entries.fold(0, (sum, entry) => sum + (entry.key.price * entry.value));


  void clearCart() {

    _items.clear();

    notifyListeners();

  }

}


// ================= SCREENS =================


class MainMenuScreen extends StatelessWidget {

  const MainMenuScreen({super.key});


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        title: const Text('Kantin SMKN 1 Banjar'),

        backgroundColor: Colors.green,

        foregroundColor: Colors.white,

        actions: [

          IconButton(

            icon: const Icon(Icons.shopping_cart),

            onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const CartScreen())),

          )

        ],

      ),

      body: ListView.builder(

        padding: const EdgeInsets.all(16),

        itemCount: listKantin.length,

        itemBuilder: (context, index) {

          final kantin = listKantin[index];

          return Card(

            child: ListTile(

              leading: Text(kantin.icon, style: const TextStyle(fontSize: 30)),

              title: Text(kantin.name),

              trailing: const Icon(Icons.chevron_right),

              onTap: () => Navigator.push(

                context,

                MaterialPageRoute(builder: (_) => DetailKantinScreen(canteen: kantin)),

              ),

            ),

          );

        },

      ),

    );

  }

}


class DetailKantinScreen extends StatelessWidget {

  final Canteen canteen;

  const DetailKantinScreen({super.key, required this.canteen});


  @override

  Widget build(BuildContext context) {

    final menuKantin = listMenu.where((item) => item.canteenId == canteen.id).toList();

    return Scaffold(

      appBar: AppBar(title: Text(canteen.name)),

      body: ListView.builder(

        itemCount: menuKantin.length,

        itemBuilder: (context, index) {

          final item = menuKantin[index];

          return ListTile(

            title: Text(item.name),

            subtitle: Text('Rp ${item.price}'),

            trailing: IconButton(

              icon: const Icon(Icons.add_shopping_cart),

              onPressed: () {

                Provider.of<CartManager>(context, listen: false).addItem(item);

                ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${item.name} ditambahkan')));

              },

            ),

          );

        },

      ),

    );

  }

}


class CartScreen extends StatelessWidget {

  const CartScreen({super.key});


  Future<void> _launchWhatsApp(BuildContext context, Map<MenuItem, int> items, int total) async {

    if (items.isEmpty) return;

    

    final firstItem = items.keys.first;

    final canteen = listKantin.firstWhere((c) => c.id == firstItem.canteenId);

    

    final buffer = StringBuffer();

    buffer.writeln('Halo, saya ingin memesan:');

    items.forEach((item, qty) {

      buffer.writeln('- ${item.name} x$qty (Rp ${item.price * qty})');

    });

    buffer.writeln('\nTotal: Rp $total');

    

    final url = Uri.parse('https://wa.me/${canteen.waNumber}?text=${Uri.encodeComponent(buffer.toString())}');

    if (await canLaunchUrl(url)) {

      await launchUrl(url, mode: LaunchMode.externalApplication);

    }

  }


  @override

  Widget build(BuildContext context) {

    final cart = Provider.of<CartManager>(context);

    return Scaffold(

      appBar: AppBar(title: const Text('Keranjang')),

      body: cart.items.isEmpty

          ? const Center(child: Text('Keranjang kosong'))

          : Column(

              children: [

                Expanded(

                  child: ListView(

                    children: cart.items.entries.map((entry) => ListTile(

                      title: Text(entry.key.name),

                      subtitle: Text('Qty: ${entry.value}'),

                      trailing: Text('Rp ${entry.key.price * entry.value}'),

                    )).toList(),

                  ),

                ),

                Padding(

                  padding: const EdgeInsets.all(16.0),

                  child: ElevatedButton(

                    onPressed: () => _l

aunchWhatsApp(context, cart.items, cart.totalHarga),

                    child: Text('Pesan via WhatsApp (Rp ${cart.totalHarga})'),

                  ),

                )

              ],

            ),

    );

  }

}


Comments

Popular posts from this blog

Latihan Tentang Appbar,column berisi 3 widget text

PRAKTIKUM FLUTTER APLIKASI JADWAL + setState

Praktikum Flutter: Aplikasi Jadwal Pelajaran Sederhana (CRUD + setState)