caching for cart

This commit is contained in:
Mikkel Troels Kongsted 2025-03-14 13:26:46 +01:00
parent 44ef95ba63
commit de4e91db72
19 changed files with 175 additions and 69 deletions

@ -1,16 +1,38 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:mobile/models/cart_item.dart';
import 'package:mobile/models/product.dart';
import 'package:mobile/results.dart';
import 'package:path_provider/path_provider.dart';
class ProductIdException implements Exception {}
class CartController extends ChangeNotifier {
abstract class CartController extends ChangeNotifier {
List<CartItem> allCartItems();
CartItem? withProductId(int productId);
void incrementAmount(int productId);
void decrementAmount(int productId);
bool willRemoveOnNextDecrement(int productId);
void removeCartItem(int productId);
void addToCart(Product product);
int totalItemsInCart();
int totalPrice();
void clearCart();
}
class CartControllerMemory extends CartController {
final List<CartItem> cart = [];
CartControllerMemory();
@override
List<CartItem> allCartItems() {
return cart;
}
@override
CartItem? withProductId(int productId) {
for (var i = 0; i < cart.length; i++) {
if (cart[i].product.id == productId) {
@ -20,6 +42,7 @@ class CartController extends ChangeNotifier {
return null;
}
@override
void incrementAmount(int productId) {
final cartItem = withProductId(productId);
if (cartItem == null) {
@ -29,6 +52,7 @@ class CartController extends ChangeNotifier {
notifyListeners();
}
@override
void decrementAmount(int productId) {
final cartItem = withProductId(productId);
if (cartItem == null) {
@ -41,6 +65,7 @@ class CartController extends ChangeNotifier {
notifyListeners();
}
@override
bool willRemoveOnNextDecrement(int productId) {
final cartItem = withProductId(productId);
if (cartItem == null) {
@ -49,6 +74,7 @@ class CartController extends ChangeNotifier {
return cartItem.amount <= 1;
}
@override
void removeCartItem(int productId) {
final cartItem = withProductId(productId);
if (cartItem == null) {
@ -58,6 +84,7 @@ class CartController extends ChangeNotifier {
notifyListeners();
}
@override
void addToCart(Product product) {
final cartItem = withProductId(product.id);
if (cartItem == null) {
@ -68,17 +95,20 @@ class CartController extends ChangeNotifier {
notifyListeners();
}
@override
int totalItemsInCart() {
return cart.fold<int>(0, (prev, cartItem) => prev + cartItem.amount);
}
@override
int totalPrice() {
return cart.fold<int>(
0,
(prev, cartItem) =>
prev + cartItem.amount * cartItem.product.priceInDkkCents);
prev + cartItem.amount * cartItem.product.priceDkkCent);
}
@override
void clearCart() {
cart.clear();
notifyListeners();
@ -89,9 +119,63 @@ class CartController extends ChangeNotifier {
}
}
class CartItem {
final Product product;
int amount;
class CartControllerCache extends CartControllerMemory {
static Future<File> get _cacheFile async {
final directory = await getApplicationCacheDirectory();
return File("${directory.path}/cart.json").create();
}
CartItem({required this.product, required this.amount});
CartControllerCache() {
load();
}
void save() async {
final json =
jsonEncode(cart.map((cartItem) => CartItem.toJson(cartItem)).toList());
(await _cacheFile).writeAsString(json);
}
void load() async {
final json = await (await _cacheFile).readAsString();
print("Loading cache: $json");
if (json.isEmpty) {
return;
}
final res = jsonDecode(json);
final cartItems = (res as List<dynamic>)
.map(((cartItems) => CartItem.fromJson(cartItems)))
.toList();
cart.insertAll(0, cartItems);
notifyListeners();
}
@override
void incrementAmount(int productId) {
super.incrementAmount(productId);
save();
}
@override
void decrementAmount(int productId) {
super.decrementAmount(productId);
save();
}
@override
void removeCartItem(int productId) {
super.removeCartItem(productId);
save();
}
@override
void addToCart(Product product) {
super.addToCart(product);
save();
}
@override
void clearCart() {
super.clearCart();
save();
}
}

@ -23,7 +23,7 @@ class ProductController extends ChangeNotifier {
}
}
get filteredProducts {
List<Product> get filteredProducts {
if (query.trim().isEmpty) {
return products;
}

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:mobile/models/cart_item.dart';
import 'package:mobile/models/product.dart';
import 'package:mobile/controllers/cart.dart';
class ReceiptController extends ChangeNotifier {
int nextId = 0;
@ -80,6 +80,6 @@ class ReceiptItem {
ReceiptItem({required this.product, required this.amount});
int totalPrice() {
return product.priceInDkkCents * amount;
return product.priceDkkCent * amount;
}
}

@ -29,7 +29,7 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => RoutingController()),
ChangeNotifierProvider(
create: (_) => ProductController(server: server)),
ChangeNotifierProvider(create: (_) => CartController()),
ChangeNotifierProvider(create: (_) => CartControllerCache()),
ChangeNotifierProvider(create: (_) => ReceiptController()),
ChangeNotifierProvider(create: (_) => PayingStateController()),
ChangeNotifierProvider(create: (_) => AddToCartStateController()),

@ -0,0 +1,19 @@
import 'package:mobile/models/product.dart';
class CartItem {
final Product product;
int amount;
CartItem({required this.product, required this.amount});
static Map<String, dynamic> toJson(CartItem cartItem) {
return {
"product": Product.toJson(cartItem.product),
"amount": cartItem.amount
};
}
CartItem.fromJson(Map<String, dynamic> json)
: product = Product.fromJson(json["product"]),
amount = json["amount"];
}

@ -4,24 +4,35 @@ class Product {
final int id;
final String name;
final String description;
final int priceInDkkCents;
final int priceDkkCent;
final Coordinate? location;
final String? barcode;
Product({
required this.id,
required this.name,
required this.priceInDkkCents,
required this.priceDkkCent,
required this.description,
this.location,
this.barcode,
});
static Map<String, dynamic> toJson(Product product) {
return {
"id": product.id,
"name": product.name,
"description": product.description,
"price_dkk_cent": product.priceDkkCent,
"location": product.location,
"barcode": product.barcode,
};
}
Product.fromJson(Map<String, dynamic> json)
: id = json["id"],
name = json["name"],
description = json["description"],
priceInDkkCents = json["price_dkk_cent"],
priceDkkCent = json["price_dkk_cent"],
location = null,
barcode = json["barcode"];
}

@ -6,30 +6,30 @@ class User {
final String name;
// balance is in øre
int balanceInDkkCents;
int balanceDkkCents;
User({
required this.id,
required this.email,
required this.name,
required this.balanceInDkkCents,
required this.balanceDkkCents,
});
User.fromJson(Map<String, dynamic> json)
: email = json["email"],
id = json["id"],
name = json["name"],
balanceInDkkCents = json["balance_dkk_cent"];
balanceDkkCents = json["balance_dkk_cent"];
void addBalanceFounds(int amount) {
balanceInDkkCents += amount;
balanceDkkCents += amount;
}
Result<int, String> pay(int amount) {
if (balanceInDkkCents < amount) {
if (balanceDkkCents < amount) {
return Err("User can not afford paying amount $amount");
}
balanceInDkkCents -= amount;
return Ok(balanceInDkkCents);
balanceDkkCents -= amount;
return Ok(balanceDkkCents);
}
}

@ -129,7 +129,7 @@ class AllProductsPage extends StatelessWidget {
itemBuilder: (_, idx) => ProductListItem(
productId: products[idx].id,
name: products[idx].name,
price: products[idx].priceInDkkCents,
price: products[idx].priceDkkCent,
productPage: ProductPage(product: products[idx]),
product: products[idx],
),

@ -13,7 +13,7 @@ import 'package:mobile/widgets/sized_card.dart';
import 'package:provider/provider.dart';
class CartItemView extends StatelessWidget {
final CartController cartRepo;
final CartControllerCache cartRepo;
final int productId;
final String name;
final int price;
@ -150,7 +150,7 @@ class CartPage extends StatelessWidget {
return Column(
children: [
Expanded(
child: Consumer<CartController>(
child: Consumer<CartControllerCache>(
builder: (_, cartRepo, __) {
final cart = cartRepo.allCartItems();
return ListView.builder(
@ -159,7 +159,7 @@ class CartPage extends StatelessWidget {
cartRepo: cartRepo,
productId: cart[idx].product.id,
name: cart[idx].product.name,
price: cart[idx].product.priceInDkkCents,
price: cart[idx].product.priceDkkCent,
amount: cart[idx].amount),
itemCount: cart.length,
);
@ -202,9 +202,9 @@ class CartPage extends StatelessWidget {
onPressed: () {
final productRepo = context
.read<ProductController>();
final CartController cartRepo =
context
.read<CartController>();
final CartControllerCache
cartRepo = context.read<
CartControllerCache>();
final productResult = productRepo
.productWithBarcode(
inputController.text);
@ -260,8 +260,8 @@ class CartPage extends StatelessWidget {
if (!context.mounted) {
return;
}
final CartController cartRepo =
context.read<CartController>();
final CartControllerCache cartRepo =
context.read<CartControllerCache>();
final productRepo =
context.read<ProductController>();
final productResult = productRepo

@ -23,7 +23,7 @@ class Dashboard extends StatelessWidget {
Widget build(BuildContext context) {
final pageIndexProvider = Provider.of<RoutingController>(context);
int currentIndex = pageIndexProvider.currentIndex;
final CartController cartRepo = context.watch<CartController>();
final CartControllerCache cartRepo = context.watch<CartControllerCache>();
return Scaffold(
bottomNavigationBar: BottomNavigationBar(

@ -14,7 +14,8 @@ class FinishShoppingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final CartController cartController = context.read<CartController>();
final CartControllerCache cartController =
context.read<CartControllerCache>();
final ReceiptController receiptRepo = context.read<ReceiptController>();
final PayingStateController payingStateRepo =
context.watch<PayingStateController>();
@ -33,7 +34,7 @@ class FinishShoppingPage extends StatelessWidget {
child: ListView.builder(
shrinkWrap: true,
itemBuilder: (_, idx) => ReceiptItemView(
pricePerAmount: cart[idx].product.priceInDkkCents,
pricePerAmount: cart[idx].product.priceDkkCent,
name: cart[idx].product.name,
amount: cart[idx].amount),
itemCount: cart.length),

@ -38,7 +38,7 @@ class HomePage extends StatelessWidget {
sessionController: sessionController,
placeholder: const CircularProgressIndicator(),
builder: (context, user) => Text(
"Saldo: ${formatDkkCents(user.balanceInDkkCents)}",
"Saldo: ${formatDkkCents(user.balanceDkkCents)}",
style: Theme.of(context).textTheme.headlineSmall))),
),
Expanded(

@ -38,7 +38,7 @@ class ProductPage extends StatelessWidget {
),
),
Text(
formatDkkCents(product.priceInDkkCents),
formatDkkCents(product.priceDkkCent),
style: const TextStyle(
fontSize: 16,
),
@ -66,7 +66,7 @@ class ProductPage extends StatelessWidget {
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
formatDkkCents(product.priceInDkkCents),
formatDkkCents(product.priceDkkCent),
style: Theme.of(context).textTheme.bodyLarge,
),
Padding(
@ -90,7 +90,7 @@ class ProductPage extends StatelessWidget {
duration: const Duration(seconds: 2),
);
ScaffoldMessenger.of(context).removeCurrentSnackBar();
final cartRepo = context.read<CartController>();
final cartRepo = context.read<CartControllerCache>();
cartRepo.addToCart(product);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
},

@ -29,7 +29,7 @@ class ReceiptView extends StatelessWidget {
shrinkWrap: true,
itemBuilder: (_, idx) => ReceiptItemView(
pricePerAmount:
receiptItems[idx].product.priceInDkkCents,
receiptItems[idx].product.priceDkkCent,
name: receiptItems[idx].product.name,
amount: receiptItems[idx].amount),
itemCount: receiptItems.length),

@ -29,7 +29,7 @@ class SaldoSettingsPage extends StatelessWidget {
sessionController: sessionController,
placeholder: const CircularProgressIndicator(),
builder: (context, user) => Text(
"Nuværende saldo: ${formatDkkCents(user.balanceInDkkCents)}",
"Nuværende saldo: ${formatDkkCents(user.balanceDkkCents)}",
style: Theme.of(context).textTheme.bodyLarge),
),
ElevatedButton.icon(

@ -11,90 +11,80 @@ class MockServer implements Server {
Product(
id: nextId++,
name: "Minimælk",
priceInDkkCents: 1200,
priceDkkCent: 1200,
description: "Konventionel minimælk med fedtprocent på 0,4%"),
Product(
id: nextId++,
name: "Letmælk",
priceInDkkCents: 1300,
priceDkkCent: 1300,
description: "Konventionel letmælk med fedtprocent på 1,5%",
location: Coordinate(x: 1800, y: 100)),
Product(
id: nextId++,
name: "Frilands Øko Supermælk",
priceInDkkCents: 2000,
priceDkkCent: 2000,
description:
"Økologisk mælk af frilandskøer med fedtprocent på 3,5%. Ikke homogeniseret eller pasteuriseret. Skaber store muskler og styrker knoglerne 💪"),
Product(
id: nextId++,
name: "Øko Gulerødder 1 kg",
priceInDkkCents: 1000,
priceDkkCent: 1000,
description: ""),
Product(
id: nextId++,
name: "Øko Agurk",
priceInDkkCents: 1000,
description: ""),
id: nextId++, name: "Øko Agurk", priceDkkCent: 1000, description: ""),
Product(
id: nextId++,
name: "Æbler 1 kg",
priceInDkkCents: 1000,
priceDkkCent: 1000,
description: ""),
Product(
id: nextId++,
name: "Basmati Ris",
priceInDkkCents: 2000,
priceDkkCent: 2000,
description: ""),
Product(
id: nextId++,
name: "Haribo Mix",
priceInDkkCents: 3000,
priceDkkCent: 3000,
description: ""),
Product(
id: nextId++, name: "Smør", priceInDkkCents: 3000, description: ""),
Product(id: nextId++, name: "Smør", priceDkkCent: 3000, description: ""),
Product(
id: nextId++,
name: "Harboe Cola",
priceInDkkCents: 500,
priceDkkCent: 500,
description: ""),
Product(
id: nextId++,
barcode: "5060337502900",
name: "Monster Energi Drik",
priceInDkkCents: 1500,
priceDkkCent: 1500,
description: ""),
Product(
id: nextId++,
barcode: "5712870659220",
name: "Amper Energi Drik",
priceInDkkCents: 750,
priceDkkCent: 750,
description: ""),
Product(
id: nextId++,
barcode: "5710326001937",
name: "Danskvand Med Brus",
priceInDkkCents: 500,
priceDkkCent: 500,
description: "Med smag a blåbær"),
Product(
id: nextId++,
name: "Spaghetti",
priceInDkkCents: 1000,
description: ""),
id: nextId++, name: "Spaghetti", priceDkkCent: 1000, description: ""),
Product(
id: nextId++,
name: "Rød Cecil",
priceInDkkCents: 6000,
description: ""),
id: nextId++, name: "Rød Cecil", priceDkkCent: 6000, description: ""),
Product(
id: nextId++,
name: "Jägermeister 750 ml",
priceInDkkCents: 12000,
priceDkkCent: 12000,
description: ""),
Product(
id: nextId++,
barcode: "5711953068881",
name: "Protein Chokoladedrik",
priceInDkkCents: 1500,
priceDkkCent: 1500,
description: "Arla's protein chokolade drik der giver store muskler"),
]);
}
@ -128,7 +118,7 @@ class MockServer implements Server {
id: 0,
email: "test@test.com",
name: "testuser",
balanceInDkkCents: 10000));
balanceDkkCents: 10000));
}
@override

@ -1,6 +1,6 @@
import 'package:intl/intl.dart';
String formatDkkCents(int priceInDkkCents) {
String formatDkkCents(int priceDkkCents) {
final formatter = NumberFormat("###,##0.00", "da_DK");
return "${formatter.format(priceInDkkCents / 100.0)} kr";
return "${formatter.format(priceDkkCents / 100.0)} kr";
}

@ -268,7 +268,7 @@ packages:
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"

@ -39,6 +39,7 @@ dependencies:
google_fonts: ^6.2.1
intl: ^0.20.2
http: ^1.3.0
path_provider: ^2.1.5
dev_dependencies:
flutter_test: