Belajar Flutter dari Awal: Manajemen State Lanjutan dengan Provider (Part 8)
Rifqi An
Halo, para pendekar keyboard dan ksatria kopi! Balik lagi nih sama gue, di seri Belajar Flutter dari Awal. Gimana kabar jari-jemari kalian? Semoga nggak kriting karena kebanyakan ngoding, ya! Kita udah sampai di Part 8 nih, gila! Nggak kerasa, kan? Dari nol sampai sekarang udah mau ngomongin state management tingkat dewa.
Oke, di part-part sebelumnya kita udah kenalan sama yang namanya Provider. Udah tahu lah ya, itu semacam bapak kos yang bisa nyediain data buat anak-anak kos (widget) di sekitarnya. Tapi gimana kalau bapak kosnya banyak? Atau data yang disediain itu kompleks banget? Nah, di sinilah kita butuh jurus baru!
Siap-siap pegangan erat, sediakan kopi (atau teh, kalau lagi diet kafein), dan mari kita selami dunia Manajemen State Lanjutan dengan Provider!
Daftar Isi
- Pendahuluan: Kenapa Harus Lanjutan?
- Review Singkat: Provider Itu Apa Sih?
- Mengapa Kita Butuh State Management yang Lebih Advanced?
- Jurus Jitu Provider untuk State yang Kompleks
- Studi Kasus: Aplikasi Toko Kopi Online (dengan Keranjang Belanja)
- Kesimpulan: Kapan Pakai yang Mana dan Manfaatnya
- Latihan: Simulasi Antrean Wartel Era 90-an
Pendahuluan: Kenapa Harus Lanjutan?
Oke, di part sebelumnya kita udah bahas dasar-dasar Provider, termasuk ChangeNotifierProvider. Kalau cuma satu Provider buat satu state sederhana, itu gampang banget, kan? Ibarat jualan gorengan sebiji, ya tinggal goreng aja. Tapi gimana kalau kita mau bikin aplikasi beneran? Aplikasi yang punya banyak fitur, banyak data, dan banyak interaksi?
Misalnya, aplikasi toko online. Ada daftar produk, ada keranjang belanja, ada data user, ada history transaksi, ada diskon, dll. Nah, kalau semua itu cuma pakai satu ChangeNotifierProvider, atau bikin ChangeNotifierProvider satu per satu di setiap widget, dijamin pusing tujuh keliling! Kodenya bakal jadi benang kusut cucian, susah di-maintain, dan gampang banget kena bug yang bikin aplikasi jadi error. Jangan sampai aplikasinya jadi kayak benang kusut cucian yang bikin pusing pas mau nyari kaos kaki kembarannya.
Makanya, kita perlu jurus-jurus lanjutan biar ngodingnya tetap santuy, rapi, dan performanya juga optimal. Nggak mau kan, aplikasi kita lemot cuma gara-gara salah manajemen state? Dijamin langsung kena review bintang satu di Play Store atau App Store, ngeri!
Review Singkat: Provider Itu Apa Sih?
Sebelum kita loncat ke yang advance, yuk kita kilas balik sebentar. Provider itu intinya adalah sebuah paket (package) di Flutter yang memudahkan kita untuk "menyediakan" (provide) data atau "mendengarkan" (consume) perubahan data di seluruh aplikasi kita.
Konsep dasarnya simpel banget:
- Kita punya sebuah data (misal, hitungan angka, daftar produk, status login).
- Data itu kita bungkus pakai
ChangeNotifier(kalau datanya bisa berubah dan kita mau UI di-update otomatis). - Terus, data yang udah dibungkus itu kita "sediakan" pakai
ChangeNotifierProviderdi pohon widget kita. - Di mana pun di bawah Provider itu, kita bisa "mengambil" atau "mendengarkan" data tersebut.
Contoh sederhana ChangeNotifierProvider yang mungkin udah kalian pakai:
// counter_provider.dart
import 'package:flutter/foundation.dart';
class CounterProvider with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Beritahu widget yang mendengarkan bahwa ada perubahan
}
}
// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter_provider.dart';
void main() {
runApp(
ChangeNotifierProvider( // Nah ini dia!
create: (context) => CounterProvider(),
child: MyApp(),
),
);
}
// di suatu widget
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Mengambil nilai count dari CounterProvider
final counter = Provider.of<CounterProvider>(context);
return Text('Count: ${counter.count}');
}
}
Gampang, kan? Tapi itu cuma buat satu. Gimana kalau seribu? Oke, seribu lebay sih, tapi kalau ada puluhan gimana?
Mengapa Kita Butuh State Management yang Lebih Advanced?
Ketika State Menggila: Banyak Banget!
Bayangin kita punya aplikasi yang udah agak besar:
AuthProvider: Untuk status login user.ProductProvider: Daftar semua produk.CartProvider: Item-item di keranjang belanja.OrderProvider: History pesanan user.ThemeProvider: Untuk tema terang/gelap aplikasi.- ...dan masih banyak lagi!
Kalau kita cuma pakai ChangeNotifierProvider satu per satu, nanti kodenya bisa jadi begini di main.dart:
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => AuthProvider(),
child: ChangeNotifierProvider(
create: (context) => ProductProvider(),
child: ChangeNotifierProvider(
create: (context) => CartProvider(),
child: ChangeNotifierProvider(
create: (context) => OrderProvider(),
child: MyApp(),
),
),
),
),
);
}
Waduh, itu namanya "Provider Hell"! Kodenya jadi kayak piramida terbalik, susah dibaca, dan kalau mau nambah satu lagi harus nambah nesting lagi. Selain itu, kalau cuma pakai Provider.of<T>(context) biasa, semua widget yang "mendengarkan" provider itu bakal di-rebuild (dibangun ulang) setiap kali ada perubahan, padahal mungkin cuma sebagian kecil dari state yang mereka butuhkan.
Nah, biar nggak pusing dan biar aplikasi kita tetap ngebut, mari kita kenalan sama teman-teman baru!
Jurus Jitu Provider untuk State yang Kompleks
MultiProvider: Solusi untuk Banyak Provider (Kayak Kondangan!)
MultiProvider itu kayak tenda kondangan besar yang isinya banyak meja prasmanan (provider). Jadi, daripada bikin tenda kecil satu-satu yang bikin sempit, mending bikin satu tenda besar aja dan semua provider masuk situ.
Dengan MultiProvider, kita bisa mendaftarkan banyak ChangeNotifierProvider (atau jenis provider lainnya) di satu tempat secara rapi.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Anggap kita punya provider-provider ini
import 'auth_provider.dart';
import 'product_provider.dart';
import 'cart_provider.dart';
import 'theme_provider.dart';
void main() {
runApp(
MultiProvider( // Ini dia jagoannya!
providers: [
ChangeNotifierProvider(create: (context) => AuthProvider()),
ChangeNotifierProvider(create: (context) => ProductProvider()),
ChangeNotifierProvider(create: (context) => CartProvider()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
],
child: MyApp(),
),
);
}
Lihat kan? Lebih rapi, lebih mudah dibaca, dan nggak ada lagi piramida terbalik yang bikin kepala nyut-nyutan. Semua provider dikumpulkan dalam satu daftar providers: [...]. Mantap jiwa!
Selector: Memilih Bagian State yang Relevan (Biar Nggak Boros!)
Selector ini adalah hero-nya performa! Bayangin kita punya data user yang besar banget (nama, alamat, email, foto profil, riwayat transaksi, dll.). Kalau kita cuma mau nampilin nama user di header aplikasi, apakah kita perlu me-rebuild seluruh widget header kalau ada data user lain yang berubah (misal, user update alamat)? Tentu tidak! Itu namanya boros sumber daya.
Selector memungkinkan kita untuk "memilih" (select) hanya bagian kecil dari state yang kita butuhkan. Widget yang menggunakan Selector hanya akan di-rebuild jika bagian state yang dipilih itu berubah.
// Contoh AuthProvider (Anggap punya property namaUser)
class AuthProvider with ChangeNotifier {
String _userName = 'John Doe';
String _userEmail = 'john.doe@example.com';
// ... data user lainnya
String get userName => _userName;
String get userEmail => _userEmail;
void updateUserName(String newName) {
_userName = newName;
notifyListeners(); // Hanya ini yang berubah, bukan email
}
// ... metode lain
}
// Cara menggunakan Selector
class UserNameDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Selector<AuthProvider, String>( // <T, R> T=tipe provider, R=tipe data yang dipilih
selector: (context, authProvider) => authProvider.userName, // Pilih hanya userName
builder: (context, userName, child) {
print('UserNameDisplay rebuild: $userName'); // Ini cuma jalan kalau userName berubah
return Text('Halo, <strong>$userName</strong>!');
},
);
}
}
// Sementara jika kita pakai Consumer biasa:
class UserEmailDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Kalau pakai Consumer, widget ini bisa di-rebuild kalau ada perubahan di AuthProvider,
// meskipun yang berubah cuma userName, bukan userEmail.
return Consumer<AuthProvider>(
builder: (context, authProvider, child) {
print('UserEmailDisplay rebuild: ${authProvider.userEmail}'); // Ini jalan kalau userName berubah juga
return Text('Emailmu: ${authProvider.userEmail}');
},
);
}
}
Keren kan Selector ini? Dia punya dua argumen penting:
selector: Sebuah fungsi yang mengambil provider dan mengembalikan data spesifik yang ingin kita "dengarkan".builder: Fungsi yang membangun UI kita, tapi hanya akan dipanggil ulang kalau nilai yang dikembalikan olehselectorberubah (berdasarkan perbandingan operator==).
Intinya, pakai Selector itu kayak lagi milih-milih barang di supermarket. Kamu cuma ambil yang kamu butuhin, nggak semua barang di rak kamu angkut. Efisien!
Consumer: Si Tua Keladi yang Tetap Berguna
Meskipun ada Selector yang canggih, Consumer yang sudah kita kenal tetap punya tempatnya. Consumer itu cara paling gampang untuk mengakses provider dan me-rebuild widget saat ada perubahan.
class MySimpleWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text('Jumlah saat ini: ${counter.count}');
},
);
}
}
Kapan pakai Consumer?
- Kalau widget yang kamu buat memang perlu di-rebuild setiap kali ada perubahan apapun di dalam provider.
- Kalau data yang kamu akses dari provider itu tunggal atau sederhana dan nggak ada risiko performa besar.
- Untuk kasus yang cepat dan tidak terlalu kompleks.
Jadi, jangan benci Consumer. Dia itu seperti teman lama yang selalu ada. Kadang kita butuh yang baru dan lebih canggih (Selector), tapi yang lama pun tetap setia dan bisa diandalkan.
Studi Kasus: Aplikasi Toko Kopi Online (dengan Keranjang Belanja)
Oke, biar nggak cuma teori, yuk kita coba bikin studi kasus sederhana: aplikasi toko kopi online! Aplikasi ini punya daftar kopi dan fitur keranjang belanja. Ini adalah skenario klasik di mana MultiProvider dan Selector akan sangat membantu.
Bikin Model Data Kopi & Item Keranjang
Kita butuh dua model sederhana:
Kopi: Untuk informasi nama dan harga kopi.ItemKeranjang: Untuk menyimpan kopi apa saja yang ada di keranjang, beserta jumlahnya.
// models/kopi.dart
class Kopi {
final String nama;
final double harga;
Kopi({required this.nama, required this.harga});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Kopi && runtimeType == other.runtimeType && nama == other.nama;
@override
int get hashCode => nama.hashCode;
}
// models/item_keranjang.dart
import 'package:provider_advanced/models/kopi.dart';
class ItemKeranjang {
final Kopi kopi;
int jumlah;
ItemKeranjang({required this.kopi, this.jumlah = 1});
double get totalHarga => kopi.harga * jumlah;
}
Kenapa ada operator == dan hashCode di kelas Kopi? Ini penting banget kalau nanti kita mau bandingin objek Kopi, misalnya saat mencari kopi di keranjang. Kalau nggak di-override, dua objek Kopi dengan nama yang sama akan dianggap berbeda oleh Dart!
ChangeNotifier untuk State Toko dan Keranjang
Kita akan punya dua ChangeNotifier:
TokoKopiProvider: Menyimpan daftar kopi yang dijual.KeranjangProvider: Menyimpan daftarItemKeranjangdan menghitung total harga.
// providers/toko_kopi_provider.dart
import 'package:flutter/foundation.dart';
import 'package:provider_advanced/models/kopi.dart';
class TokoKopiProvider with ChangeNotifier {
final List<Kopi> _daftarKopi = [
Kopi(nama: 'Espresso', harga: 25000),
Kopi(nama: 'Latte', harga: 30000),
Kopi(nama: 'Cappuccino', harga: 32000),
Kopi(nama: 'Americano', harga: 28000),
Kopi(nama: 'Mochaccino', harga: 35000),
];
List<Kopi> get daftarKopi => _daftarKopi;
}
// providers/keranjang_provider.dart
import 'package:flutter/foundation.dart';
import 'package:provider_advanced/models/item_keranjang.dart';
import 'package:provider_advanced/models/kopi.dart';
class KeranjangProvider with ChangeNotifier {
final List<ItemKeranjang> _items = [];
List<ItemKeranjang> get items => _items;
double get totalHarga {
return _items.fold(0.0, (sum, item) => sum + item.totalHarga);
}
int get jumlahItem => _items.fold(0, (sum, item) => sum + item.jumlah);
void tambahKeKeranjang(Kopi kopi) {
int index = _items.indexWhere((item) => item.kopi == kopi);
if (index != -1) {
_items[index].jumlah++;
} else {
_items.add(ItemKeranjang(kopi: kopi));
}
notifyListeners();
}
void hapusDariKeranjang(Kopi kopi) {
int index = _items.indexWhere((item) => item.kopi == kopi);
if (index != -1) {
if (_items[index].jumlah > 1) {
_items[index].jumlah--;
} else {
_items.removeAt(index);
}
notifyListeners();
}
}
void clearKeranjang() {
_items.clear();
notifyListeners();
}
}
Implementasi MultiProvider
Sekarang, kita gabungkan kedua provider ini di main.dart menggunakan MultiProvider.
// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_advanced/providers/keranjang_provider.dart';
import 'package:provider_advanced/providers/toko_kopi_provider.dart';
import 'package:provider_advanced/screens/home_screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider( // Ini dia MultiProvider-nya
providers: [
ChangeNotifierProvider(create: (context) => TokoKopiProvider()),
ChangeNotifierProvider(create: (context) => KeranjangProvider()),
],
child: MaterialApp(
title: 'Toko Kopi Bang Jago',
theme: ThemeData(
primarySwatch: Colors.brown,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomeScreen(),
),
);
}
}
Lihat, kan? main.dart kita tetap rapi meskipun ada dua provider. Kalau besok nambah AuthProvider atau ThemeProvider, tinggal tambahin aja di list providers!
Ngoding UI dengan Selector dan Consumer
Sekarang, kita akan bangun UI untuk menampilkan daftar kopi dan keranjang belanja.
// screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; // Untuk format mata uang
import 'package:provider/provider.dart';
import 'package:provider_advanced/models/item_keranjang.dart';
import 'package:provider_advanced/models/kopi.dart';
import 'package:provider_advanced/providers/keranjang_provider.dart';
import 'package:provider_advanced/providers/toko_kopi_provider.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final keranjangProvider = Provider.of<KeranjangProvider>(context, listen: false);
return Scaffold(
appBar: AppBar(
title: Text('Toko Kopi Bang Jago'),
actions: [
IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: () {
// Show keranjang belanja
showModalBottomSheet(
context: context,
builder: (context) => KeranjangSheet(),
);
},
),
// Menggunakan Selector untuk menampilkan jumlah item di keranjang
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Center(
child: Selector<KeranjangProvider, int>(
selector: (context, keranjang) => keranjang.jumlahItem,
builder: (context, jumlahItem, child) {
return Text(
jumlahItem > 0 ? '$jumlahItem' : '',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
);
},
),
),
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Pilih Kopi Favoritmu!',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
Expanded(
child: Consumer<TokoKopiProvider>( // Menggunakan Consumer untuk daftar kopi
builder: (context, tokoKopi, child) {
return ListView.builder(
itemCount: tokoKopi.daftarKopi.length,
itemBuilder: (context, index) {
final kopi = tokoKopi.daftarKopi[index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: Icon(Icons.coffee),
title: Text(kopi.nama),
subtitle: Text(NumberFormat.currency(
locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0)
.format(kopi.harga)),
trailing: IconButton(
icon: Icon(Icons.add_shopping_cart),
onPressed: () {
keranjangProvider.tambahKeKeranjang(kopi);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${kopi.nama} ditambahkan!')),
);
},
),
),
);
},
);
},
),
),
],
),
);
}
}
class KeranjangSheet extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0),
child: Consumer<KeranjangProvider>( // Consumer untuk seluruh keranjang
builder: (context, keranjang, child) {
if (keranjang.items.isEmpty) {
return Center(
child: Text('Keranjang kosong, yuk belanja!', style: TextStyle(fontSize: 18)),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Isi Keranjangmu:', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
SizedBox(height: 10),
Expanded(
child: ListView.builder(
itemCount: keranjang.items.length,
itemBuilder: (context, index) {
final item = keranjang.items[index];
return ListTile(
title: Text('${item.kopi.nama} x${item.jumlah}'),
subtitle: Text(NumberFormat.currency(
locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0)
.format(item.totalHarga)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.remove_circle),
onPressed: () => keranjang.hapusDariKeranjang(item.kopi),
),
IconButton(
icon: Icon(Icons.add_circle),
onPressed: () => keranjang.tambahKeKeranjang(item.kopi),
),
],
),
);
},
),
),
Divider(),
// Menggunakan Selector untuk total harga, biar hanya di-rebuild kalau harga berubah
Selector<KeranjangProvider, double>(
selector: (context, keranjang) => keranjang.totalHarga,
builder: (context, totalHarga, child) {
return Text(
'Total: ${NumberFormat.currency(
locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0)
.format(totalHarga)}',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
);
},
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Selamat! Pesananmu sedang diproses!')),
);
keranjang.clearKeranjang();
Navigator.pop(context);
},
child: Text('Checkout'),
),
ElevatedButton(
onPressed: () {
keranjang.clearKeranjang();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Keranjang dikosongkan!')),
);
},
child: Text('Kosongkan Keranjang'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
),
),
],
)
],
);
},
),
);
}
}
Perhatikan beberapa poin penting di kode di atas:
- Di
AppBar, untuk menampilkan jumlah item di keranjang, kita pakaiSelector<KeranjangProvider, int>. Ini penting karena kita hanya butuh nilaijumlahItemdariKeranjangProvider. Kalau ada perubahan lain diKeranjangProvider(misal, update jumlah item yang sudah ada), tapijumlahItem-nya nggak berubah, widget jumlah di AppBar ini nggak akan di-rebuild. Hemat daya! - Untuk daftar kopi, kita pakai
Consumer<TokoKopiProvider>. Karena daftar kopi ini statis (nggak berubah-ubah setelah di-load), sebenarnya bisa juga pakaiProvider.of<TokoKopiProvider>(context, listen: false)di atasbuildmethod, tapi pakaiConsumerdi sini juga nggak masalah karena dia cuma satu kali ambil data. - Di
KeranjangSheet, untuk menampilkan seluruh isi keranjang, kita pakaiConsumer<KeranjangProvider>. Ini karena kita butuh semua data item keranjang. - Tapi, untuk menampilkan total harga di
KeranjangSheet, kita kembali pakaiSelector<KeranjangProvider, double>. Kenapa? Karena total harga itu cuma angka, dan widgetTextyang menampilkannya cuma perlu di-rebuild kalau angka total harganya berubah. Jadi, kalau misalnya user nambah kopi yang udah ada di keranjang (jumlahnya nambah, tapi list item-nya tidak berubah strukturnya), total harga akan berubah, danSelectorini akan me-rebuild hanya bagianTexttotal harga, bukan seluruhKeranjangSheet.
Kombinasi MultiProvider, Selector, dan Consumer ini bener-bener jurus ampuh untuk bikin aplikasi Flutter yang kompleks tapi tetap performant dan mudah di-maintain. Kita bisa memilih kapan harus "mendengarkan" seluruh perubahan (Consumer) atau hanya bagian spesifik (Selector), dan MultiProvider bikin semua penataan provider jadi rapi!
Kesimpulan: Kapan Pakai yang Mana dan Manfaatnya
Oke, jadi kita udah belajar banyak hari ini, dari yang tadinya cuma ChangeNotifierProvider sendirian, sekarang udah kenalan sama teman-temannya yang lebih canggih.
MultiProvider: Wajib banget dipakai kalau aplikasi kalian punya lebih dari satu provider. Ini solusi paling rapi dan bersih untuk mendaftarkan banyak provider di satu tempat (biasanya di paling atas pohon widget, seperti dimain.dart). Hindari "Provider Hell" dengan menumpukChangeNotifierProviderberlapis-lapis!Selector: Ini pilihan utama kalau kamu mau mengoptimalkan performa. GunakanSelectorsaat kamu hanya butuh sebagian kecil dari state dalam sebuah provider, dan kamu ingin widgetmu hanya di-rebuild ketika bagian spesifik itu berubah. Ini penting banget untuk widget yang sering di-rebuild atau di aplikasi yang performanya kritis.Consumer: Masih sangat berguna dan mudah digunakan. PakaiConsumerkalau widgetmu memang perlu di-rebuild setiap kali ada perubahan apapun di dalam provider, atau kalau state yang kamu akses itu tunggal dan sederhana. Untuk kasus yang cepat dan tidak terlalu kompleks,Consumersudah lebih dari cukup.
Dengan menguasai ketiga jurus ini, kalian sudah siap menghadapi tantangan manajemen state di aplikasi Flutter yang lebih besar dan kompleks. Nggak ada lagi drama ngoding yang bikin kepala mau pecah karena state berantakan. Ingat, state management yang baik itu bukan cuma soal rapi, tapi juga soal performa dan kemudahan dalam pengembangan di masa depan.
Selamat ngoding, jangan lupa istirahat, dan ngopi biar tetap waras! Sampai jumpa di part selanjutnya, ya!
Latihan: Simulasi Antrean Wartel Era 90-an
Oke, biar ilmu yang tadi nggak cuma numpang lewat di kepala, yuk kita bikin sebuah simulasi sederhana yang kocak tapi butuh state management lanjutan. Tugas kalian adalah membuat simulasi "Antrean Wartel Era 90-an"!
Skenario: Di sebuah wartel, ada beberapa bilik telepon (misal 3 bilik). Orang-orang datang dan masuk antrean. Begitu ada bilik yang kosong, orang di antrean paling depan bisa masuk. Kalian harus menampilkan status bilik, daftar antrean, dan mungkin total orang di antrean.
Fitur yang Harus Ada:
- Daftar Bilik Telepon: Tampilkan 3 bilik. Setiap bilik bisa berstatus "Kosong" atau "Terisi oleh [Nama Pengantre]".
- Tombol "Antre Dong, Mba/Mas!": Ketika tombol ini ditekan, seorang "pengantre baru" (dengan nama acak atau nama yang diinput) akan ditambahkan ke daftar antrean.
- Daftar Antrean: Tampilkan siapa saja yang sedang mengantre, beserta nomor urutnya.
- Tombol "Panggilan Selesai (Bilik X)": Setiap bilik yang terisi harus punya tombol untuk menandakan panggilan selesai. Ketika tombol ini ditekan, bilik akan kosong, dan pengantre berikutnya dari daftar antrean akan masuk ke bilik tersebut (jika ada).
- Info Total Antrean: Tampilkan berapa total orang yang sedang mengantre.
Requirement Spesial:
- Gunakan
MultiProvideruntuk mengelola state. Kalian mungkin butuh setidaknya dua provider:BilikProvider: Mengelola status masing-masing bilik (siapa yang pakai, durasi panggilan, dll).AntreanProvider: Mengelola daftar orang yang sedang mengantre.
- Gunakan
Selectordi beberapa bagian UI untuk memastikan hanya bagian yang relevan yang di-rebuild. Contoh:- Menampilkan status bilik (
Selectoruntuk status bilik tertentu). - Menampilkan total orang di antrean (
Selectoruntuk jumlah antrean).
- Menampilkan status bilik (
- Gunakan
Consumerdi bagian UI yang memang perlu mendengarkan semua perubahan dari provider (misal, daftar lengkap antrean). - Jangan lupa humor receh! Mungkin nama pengantre acak bisa kayak "Bapak RT", "Ibu Arisan", "Mas Jomblo", atau "Pak Satpam".
Ini akan jadi latihan yang seru dan menantang untuk mengaplikasikan apa yang sudah kita pelajari. Selamat mencoba, para programmer tangguh! Jangan sampai aplikasimu ngadat kayak pulsa wartel habis di tengah obrolan penting!
.png)