Skip to content

Commit

Permalink
Refactor AddToCartController to use a separate ItemQuantityController…
Browse files Browse the repository at this point in the history
… that is less error prone

# Conflicts:
#	ecommerce_app/lib/src/features/cart/presentation/add_to_cart/add_to_cart_controller.g.dart
#	ecommerce_app/test/src/features/cart/presentation/add_to_cart/add_to_cart_controller_test.dart
  • Loading branch information
bizz84 committed Jan 16, 2024
1 parent 75c6213 commit fb00703
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,30 @@ part 'add_to_cart_controller.g.dart';
@riverpod
class AddToCartController extends _$AddToCartController {
@override
FutureOr<int> build() {
return 1;
}

void updateQuantity(int quantity) {
state = AsyncData(quantity);
FutureOr<void> build() {
// nothing to do
}

Future<void> addItem(ProductID productId) async {
final cartService = ref.read(cartServiceProvider);
final item = Item(productId: productId, quantity: state.value!);
state = const AsyncLoading<int>().copyWithPrevious(state);
final value = await AsyncValue.guard(() => cartService.addItem(item));
if (value.hasError) {
state = AsyncError(value.error!, StackTrace.current);
} else {
state = const AsyncData(1);
final quantity = ref.read(itemQuantityControllerProvider);
final item = Item(productId: productId, quantity: quantity);
state = const AsyncLoading<void>();
state = await AsyncValue.guard(() => cartService.addItem(item));
if (!state.hasError) {
ref.read(itemQuantityControllerProvider.notifier).updateQuantity(1);
}
}
}

@riverpod
class ItemQuantityController extends _$ItemQuantityController {
@override
int build() {
return 1;
}

void updateQuantity(int quantity) {
state = quantity;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ class AddToCartWidget extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<AsyncValue<int>>(
ref.listen<AsyncValue>(
addToCartControllerProvider,
(_, state) => state.showAlertDialogOnError(context),
);
final availableQuantity = ref.watch(itemAvailableQuantityProvider(product));
final state = ref.watch(addToCartControllerProvider);
final quantity = ref.watch(itemQuantityControllerProvider);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expand All @@ -34,14 +35,14 @@ class AddToCartWidget extends ConsumerWidget {
children: [
Text('Quantity:'.hardcoded),
ItemQuantitySelector(
quantity: state.value!,
quantity: quantity,
// let the user choose up to the available quantity or
// 10 items at most
maxQuantity: min(availableQuantity, 10),
onChanged: state.isLoading
? null
: (quantity) => ref
.read(addToCartControllerProvider.notifier)
.read(itemQuantityControllerProvider.notifier)
.updateQuantity(quantity),
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,40 +34,54 @@ void main() {
(_) => Future.value(null),
);
final container = makeProviderContainer(cartService);
final controller = container.read(addToCartControllerProvider.notifier);
final listener = Listener<AsyncValue<int>>();
// add to cart controller
final addToCartController =
container.read(addToCartControllerProvider.notifier);
final addToCartListener = Listener<AsyncValue<void>>();
container.listen(
addToCartControllerProvider,
listener.call,
addToCartListener.call,
fireImmediately: true,
);
// item quantity controller
final itemQuantityController =
container.read(itemQuantityControllerProvider.notifier);
final itemQuantityListener = Listener<int>();
container.listen(
itemQuantityControllerProvider,
itemQuantityListener.call,
fireImmediately: true,
);
// run
const initialData = AsyncData<int>(1);
const initialData = AsyncData<void>(null);
// the build method returns a value immediately, so we expect AsyncData
verify(() => listener(null, initialData));
verify(() => addToCartListener(null, initialData));
verify(() => itemQuantityListener(null, 1));
// update quantity
controller.updateQuantity(quantity);
itemQuantityController.updateQuantity(quantity);
// the quantity is updated
verify(() => listener(initialData, const AsyncData<int>(quantity)));
verify(() => itemQuantityListener(1, quantity));
// add item
await controller.addItem(productId);
await addToCartController.addItem(productId);
verifyInOrder(
[
// the loading state is set
() => listener(
const AsyncData<int>(quantity),
const AsyncLoading<int>()
.copyWithPrevious(const AsyncData<int>(quantity)),
() => addToCartListener(
initialData,
any(that: isA<AsyncLoading>()),
),
// then the data is set with quantity: 1
() => listener(
const AsyncLoading<int>()
.copyWithPrevious(const AsyncData<int>(quantity)),
() => addToCartListener(
any(that: isA<AsyncLoading>()),
initialData,
),
],
);
verifyNoMoreInteractions(listener);
// on success, quantity goes back to 1
verify(() => itemQuantityListener(quantity, 1));
// then, no more interactions
verifyNoMoreInteractions(addToCartListener);
verifyNoMoreInteractions(itemQuantityListener);
verify(() => cartService.addItem(item)).called(1);
});

Expand All @@ -78,41 +92,53 @@ void main() {
when(() => cartService.addItem(item))
.thenThrow((_) => Exception('Connection failed'));
final container = makeProviderContainer(cartService);
final controller = container.read(addToCartControllerProvider.notifier);
final listener = Listener<AsyncValue<int>>();
// add to cart controller
final addToCartController =
container.read(addToCartControllerProvider.notifier);
final addToCartListener = Listener<AsyncValue<void>>();
container.listen(
addToCartControllerProvider,
listener.call,
addToCartListener.call,
fireImmediately: true,
);
const initialData = AsyncData<int>(1);
// item quantity controller
final itemQuantityController =
container.read(itemQuantityControllerProvider.notifier);
final itemQuantityListener = Listener<int>();
container.listen(
itemQuantityControllerProvider,
itemQuantityListener.call,
fireImmediately: true,
);
// run
const initialData = AsyncData<void>(null);
// the build method returns a value immediately, so we expect AsyncData
verify(() => listener(null, initialData));
verify(() => addToCartListener(null, initialData));
verify(() => itemQuantityListener(null, 1));
// update quantity
controller.updateQuantity(quantity);
itemQuantityController.updateQuantity(quantity);
// the quantity is updated
verify(
() => listener(initialData, const AsyncData<int>(quantity)),
);
verify(() => itemQuantityListener(1, quantity));
// add item
await controller.addItem(productId);
await addToCartController.addItem(productId);
verifyInOrder(
[
// the loading state is set
() => listener(
const AsyncData<int>(quantity),
const AsyncLoading<int>()
.copyWithPrevious(const AsyncData<int>(quantity)),
() => addToCartListener(
initialData,
any(that: isA<AsyncLoading>()),
),
// then an error is set
() => listener(
const AsyncLoading<int>()
.copyWithPrevious(const AsyncData<int>(quantity)),
// then the data is set with quantity: 1
() => addToCartListener(
any(that: isA<AsyncLoading>()),
any(that: isA<AsyncError>()),
),
],
);
verifyNoMoreInteractions(listener);
// on error, quantity doesn't change
// then, no more interactions
verifyNoMoreInteractions(addToCartListener);
verifyNoMoreInteractions(itemQuantityListener);
verify(() => cartService.addItem(item)).called(1);
});
});
Expand Down

0 comments on commit fb00703

Please sign in to comment.