1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
| // lib/main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
Get.put(CartController()); // global injection at startup
runApp(const GetMaterialApp(home: CatalogPage()));
}
// ── Model ──────────────────────────────────────────────────────────────────────
class Product {
final int id;
final String name;
final double price;
const Product({required this.id, required this.name, required this.price});
}
// ── Repository (simulates API) ─────────────────────────────────────────────────
class ProductRepository {
Future<List<Product>> fetchAll() async {
await Future.delayed(const Duration(milliseconds: 600));
return [
const Product(id: 1, name: 'Mechanical keyboard', price: 89.99),
const Product(id: 2, name: 'Ergonomic mouse', price: 45.00),
const Product(id: 3, name: '27" Monitor', price: 299.00),
const Product(id: 4, name: 'USB Headset', price: 65.50),
const Product(id: 5, name: '7-port USB-C Hub', price: 38.00),
];
}
}
// ── Cart controller ────────────────────────────────────────────────────────────
class CartController extends GetxController {
// RxList: any widget using .obs rebuilds when the list changes
final items = <Product>[].obs;
int get totalItems => items.length;
double get totalPrice => items.fold(0, (sum, p) => sum + p.price);
void add(Product product) => items.add(product);
void remove(Product product) => items.remove(product);
bool contains(Product product) => items.any((p) => p.id == product.id);
}
// ── Catalog controller ─────────────────────────────────────────────────────────
class CatalogController extends GetxController {
final _repo = ProductRepository();
final products = <Product>[].obs;
final isLoading = true.obs;
final errorMessage = ''.obs;
@override
void onInit() {
super.onInit();
_loadProducts();
}
Future<void> _loadProducts() async {
try {
isLoading(true);
errorMessage('');
products.assignAll(await _repo.fetchAll());
} catch (e) {
errorMessage('Error loading products: $e');
} finally {
isLoading(false);
}
}
}
// ── Catalog screen ─────────────────────────────────────────────────────────────
class CatalogPage extends StatelessWidget {
const CatalogPage({super.key});
@override
Widget build(BuildContext context) {
// Get.put inside the screen: destroyed on exit (fenix = false by default)
final catalog = Get.put(CatalogController());
final cart = Get.find<CartController>();
return Scaffold(
appBar: AppBar(
title: const Text('Catalog'),
actions: [
// Obx only rebuilds this IconButton when totalItems changes
Obx(() => Badge(
isLabelVisible: cart.totalItems > 0,
label: Text('${cart.totalItems}'),
child: IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () => Get.to(() => const CartPage()),
),
)),
const SizedBox(width: 8),
],
),
body: Obx(() {
if (catalog.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (catalog.errorMessage.isNotEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(catalog.errorMessage.value),
TextButton(onPressed: catalog._loadProducts, child: const Text('Retry')),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: catalog.products.length,
itemBuilder: (_, i) => _ProductTile(product: catalog.products[i]),
);
}),
);
}
}
class _ProductTile extends StatelessWidget {
final Product product;
const _ProductTile({required this.product});
@override
Widget build(BuildContext context) {
final cart = Get.find<CartController>();
return Card(
child: ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
trailing: Obx(() => cart.contains(product)
? FilledButton.icon(
onPressed: () => cart.remove(product),
icon: const Icon(Icons.remove_shopping_cart),
label: const Text('Remove'),
style: FilledButton.styleFrom(backgroundColor: Colors.red),
)
: FilledButton.icon(
onPressed: () => cart.add(product),
icon: const Icon(Icons.add_shopping_cart),
label: const Text('Add'),
)),
),
);
}
}
// ── Cart screen ────────────────────────────────────────────────────────────────
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override
Widget build(BuildContext context) {
final cart = Get.find<CartController>();
return Scaffold(
appBar: AppBar(title: const Text('Cart')),
body: Obx(() {
if (cart.items.isEmpty) {
return const Center(child: Text('Your cart is empty'));
}
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (_, i) {
final p = cart.items[i];
return ListTile(
title: Text(p.name),
subtitle: Text('\$${p.price.toStringAsFixed(2)}'),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => cart.remove(p),
),
);
},
),
),
const Divider(),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Total (${cart.totalItems} items)',
style: const TextStyle(fontSize: 16)),
Obx(() => Text(
'\$${cart.totalPrice.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
)),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
child: FilledButton(
onPressed: () {
cart.items.clear();
Get.back();
Get.snackbar('Order placed', 'Thank you for your purchase!',
snackPosition: SnackPosition.BOTTOM);
},
style: FilledButton.styleFrom(minimumSize: const Size(double.infinity, 48)),
child: const Text('Confirm order'),
),
),
],
);
}),
);
}
}
|