diff --git a/lib/utils/navigation.dart b/lib/utils/navigation.dart new file mode 100644 index 0000000..7e5fe23 --- /dev/null +++ b/lib/utils/navigation.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; + +extension AppNavigation on BuildContext { + void popOrGo(String fallbackLocation) { + if (canPop()) { + pop(); + return; + } + + go(fallbackLocation); + } +} diff --git a/lib/views/bar_screen_view.dart b/lib/views/bar_screen_view.dart index a48c8b3..7853623 100644 --- a/lib/views/bar_screen_view.dart +++ b/lib/views/bar_screen_view.dart @@ -53,13 +53,13 @@ class _BarScreenViewState extends State { actions: [ IconButton( tooltip: l10n.manageProducts, - onPressed: () => context.go('/products'), + onPressed: () => context.push('/products'), icon: const Icon(Icons.inventory_2_outlined), ), const SizedBox(width: 6), IconButton( tooltip: l10n.tabHistory, - onPressed: () => context.go('/history'), + onPressed: () => context.push('/history'), icon: const Icon(Icons.history_rounded), ), const SizedBox(width: 6), @@ -71,7 +71,7 @@ class _BarScreenViewState extends State { const SizedBox(width: 6), IconButton( tooltip: l10n.settings, - onPressed: () => context.go('/settings'), + onPressed: () => context.push('/settings'), icon: const Icon(Icons.settings), ), if (context.watch().isPinSet) ...[ @@ -230,7 +230,7 @@ class _ProductGrid extends StatelessWidget { ), const SizedBox(height: 20), FilledButton.icon( - onPressed: () => context.go('/products/new'), + onPressed: () => context.push('/products/new'), icon: const Icon(Icons.add), label: Text(l10n.addProduct), ), @@ -324,7 +324,7 @@ class _ProductGrid extends StatelessWidget { enabled: hasSelectedTab, onTap: () => onProductTap(product), onLongPress: () => - context.go('/products/${product.id}/edit'), + context.push('/products/${product.id}/edit'), ); }, ); diff --git a/lib/views/dev_menu_view.dart b/lib/views/dev_menu_view.dart index a44e596..7997ed7 100644 --- a/lib/views/dev_menu_view.dart +++ b/lib/views/dev_menu_view.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import '../utils/app_update_util.dart'; +import '../utils/navigation.dart'; import '../viewmodels/dev_menu_view_model.dart'; import '../l10n/app_localizations.dart'; import '../l10n/app_localizations_helpers.dart'; @@ -58,7 +59,7 @@ class _DevMenuViewState extends State { return Scaffold( appBar: AppBar( leading: IconButton( - onPressed: () => context.pop(), + onPressed: () => context.popOrGo('/bar'), icon: const Icon(Icons.arrow_back), ), title: Text(l10n.devMenu), diff --git a/lib/views/error_screen_view.dart b/lib/views/error_screen_view.dart index 13b4287..2c271fe 100644 --- a/lib/views/error_screen_view.dart +++ b/lib/views/error_screen_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../l10n/app_localizations.dart'; +import '../utils/navigation.dart'; class ErrorScreenView extends StatelessWidget { const ErrorScreenView({super.key}); @@ -14,7 +15,7 @@ class ErrorScreenView extends StatelessWidget { return Scaffold( appBar: AppBar( leading: IconButton( - onPressed: () => context.pop(), + onPressed: () => context.popOrGo('/bar'), icon: const Icon(Icons.arrow_back), ), title: Text(l10n.errorScreen), diff --git a/lib/views/history_screen_view.dart b/lib/views/history_screen_view.dart index a323a8f..f82361d 100644 --- a/lib/views/history_screen_view.dart +++ b/lib/views/history_screen_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:kooltab2/views/widgets/closed_tab_card.dart'; import 'package:provider/provider.dart'; import '../app/router.dart'; +import '../utils/navigation.dart'; import '../viewmodels/history_view_model.dart'; import '../l10n/app_localizations.dart'; import '../l10n/app_localizations_helpers.dart'; @@ -61,16 +61,11 @@ class _HistoryScreenViewState extends State with RouteAware { return Scaffold( appBar: AppBar( - title: Row( - children: [ - IconButton( - onPressed: () => context.go('/bar'), - icon: const Icon(Icons.arrow_back), - ), - const SizedBox(width: 5), - Text(l10n.tabHistory), - ], + leading: IconButton( + onPressed: () => context.popOrGo('/bar'), + icon: const Icon(Icons.arrow_back), ), + title: Text(l10n.tabHistory), actionsPadding: const EdgeInsets.symmetric(horizontal: 8), actions: [ IconButton( diff --git a/lib/views/product_form_view.dart b/lib/views/product_form_view.dart index e13551f..e9a1baa 100644 --- a/lib/views/product_form_view.dart +++ b/lib/views/product_form_view.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -12,6 +11,7 @@ import '../models/product.dart'; import '../viewmodels/product_list_view_model.dart'; import '../l10n/app_localizations.dart'; import '../l10n/app_localizations_helpers.dart'; +import '../utils/navigation.dart'; class ProductFormView extends StatefulWidget { final String? productId; @@ -70,7 +70,7 @@ class _ProductFormViewState extends State { context, ).showSnackBar(SnackBar(content: Text(l10n.productNotFound))); - context.go('/products'); + _leaveProductForm(); return; } @@ -90,10 +90,14 @@ class _ProductFormViewState extends State { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.couldNotLoadProduct))); - context.go('/products'); + _leaveProductForm(); } } + void _leaveProductForm() { + context.popOrGo('/products'); + } + @override void dispose() { _nameController.dispose(); @@ -214,7 +218,7 @@ class _ProductFormViewState extends State { if (!mounted) return; setState(() => _isSaving = false); - context.go('/products'); + _leaveProductForm(); } Future _delete() async { @@ -241,7 +245,7 @@ class _ProductFormViewState extends State { }, ); - if (confirmed != true) return; + if (confirmed != true || !mounted) return; final viewModel = context.read(); @@ -258,7 +262,7 @@ class _ProductFormViewState extends State { if (!mounted) return; - context.go('/products'); + _leaveProductForm(); } Widget _buildImagePicker(BuildContext context) { @@ -328,7 +332,7 @@ class _ProductFormViewState extends State { title: Text(title), leading: IconButton( icon: const Icon(Icons.arrow_back), - onPressed: () => context.go('/products'), + onPressed: _leaveProductForm, ), actions: [ if (widget.isEditing) diff --git a/lib/views/product_list_view.dart b/lib/views/product_list_view.dart index 29e7dad..37589ff 100644 --- a/lib/views/product_list_view.dart +++ b/lib/views/product_list_view.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import '../viewmodels/product_list_view_model.dart'; import '../l10n/app_localizations.dart'; import '../l10n/app_localizations_helpers.dart'; +import '../utils/navigation.dart'; class ProductListView extends StatefulWidget { const ProductListView({super.key}); @@ -32,20 +33,15 @@ class _ProductListViewState extends State { return Scaffold( appBar: AppBar( - title: Row( - children: [ - IconButton( - onPressed: () => context.go('/bar'), - icon: const Icon(Icons.arrow_back), - ), - const SizedBox(width: 8), - Text(l10n.products), - ], + leading: IconButton( + onPressed: () => context.popOrGo('/bar'), + icon: const Icon(Icons.arrow_back), ), + title: Text(l10n.products), ), resizeToAvoidBottomInset: false, floatingActionButton: FloatingActionButton.extended( - onPressed: () => context.go('/products/new'), + onPressed: () => context.push('/products/new'), icon: const Icon(Icons.add), label: Text(l10n.addProduct), ), @@ -102,7 +98,7 @@ class _ProductListViewState extends State { ), ), trailing: const Icon(Icons.chevron_right), - onTap: () => context.go('/products/${product.id}/edit'), + onTap: () => context.push('/products/${product.id}/edit'), ), ); }, diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index d8e3a2d..14d29a2 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -9,6 +9,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import '../models/settings.dart'; import '../utils/app_update_util.dart'; +import '../utils/navigation.dart'; import '../viewmodels/pin_lock_view_model.dart'; import '../viewmodels/settings_view_model.dart'; import '../l10n/app_localizations.dart'; @@ -197,16 +198,11 @@ class _SettingsScreenViewState extends State { return Scaffold( appBar: AppBar( - title: Row( - children: [ - IconButton( - onPressed: () => context.go('/bar'), - icon: const Icon(Icons.arrow_back), - ), - const SizedBox(width: 5), - Text(l10n.settingsTitle), - ], + leading: IconButton( + onPressed: () => context.popOrGo('/bar'), + icon: const Icon(Icons.arrow_back), ), + title: Text(l10n.settingsTitle), ), body: Builder( builder: (context) { diff --git a/lib/views/update_progress_view.dart b/lib/views/update_progress_view.dart index ce2c552..48d9ddb 100644 --- a/lib/views/update_progress_view.dart +++ b/lib/views/update_progress_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import '../utils/app_update_util.dart'; +import '../utils/navigation.dart'; import '../viewmodels/update_progress_view_model.dart'; import '../l10n/app_localizations.dart'; import '../l10n/app_localizations_helpers.dart'; @@ -60,7 +60,7 @@ class _UpdateProgressViewState extends State { TextButton( onPressed: () { Navigator.pop(context); - this.context.go('/bar'); + this.context.popOrGo('/bar'); }, child: Text(l10n.later), ), @@ -104,40 +104,46 @@ class _UpdateProgressViewState extends State { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => _showCancelDialog(), + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (!didPop) _showCancelDialog(); + }, + child: Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => _showCancelDialog(), + ), + title: Text(l10n.updating), + centerTitle: true, + automaticallyImplyLeading: false, ), - title: Text(l10n.updating), - centerTitle: true, - automaticallyImplyLeading: false, - ), - body: ListenableBuilder( - listenable: _vm, - builder: (context, child) { - return Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 480), - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildHeader(), - const SizedBox(height: 40), - _buildProgress(_vm), - const SizedBox(height: 24), - _buildStatus(_vm), - const Spacer(), - _buildVersionInfo(_vm), - ], + body: ListenableBuilder( + listenable: _vm, + builder: (context, child) { + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 480), + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildHeader(), + const SizedBox(height: 40), + _buildProgress(_vm), + const SizedBox(height: 24), + _buildStatus(_vm), + const Spacer(), + _buildVersionInfo(_vm), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ); } @@ -298,7 +304,7 @@ class _UpdateProgressViewState extends State { onPressed: () { Navigator.pop(context); _vm.cancel(); - this.context.go('/bar'); + this.context.popOrGo('/bar'); }, child: Text(l10n.cancelUpdateAction), ), diff --git a/test/product_navigation_test.dart b/test/product_navigation_test.dart new file mode 100644 index 0000000..d0a6a61 --- /dev/null +++ b/test/product_navigation_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:kooltab2/l10n/app_localizations.dart'; +import 'package:kooltab2/models/product.dart'; +import 'package:kooltab2/views/widgets/product_tile.dart'; + +void main() { + testWidgets('long-pressing a product tile returns to the bar after edit', ( + tester, + ) async { + final product = Product( + id: 'product-1', + name: 'Cola', + category: 'Drinks', + stockQuantity: 5, + lowStockThreshold: 1, + priceInCents: 250, + ); + + final router = GoRouter( + initialLocation: '/bar', + routes: [ + GoRoute( + path: '/bar', + builder: (context, state) => Scaffold( + body: ProductTile( + product: product, + enabled: false, + onTap: () {}, + onLongPress: () => context.push('/products/${product.id}/edit'), + ), + ), + ), + GoRoute( + path: '/products', + builder: (context, state) => const Scaffold(body: Text('Products')), + routes: [ + GoRoute( + path: ':id/edit', + builder: (context, state) => + const Scaffold(body: Text('Edit product')), + ), + ], + ), + ], + ); + + await tester.pumpWidget( + MaterialApp.router( + routerConfig: router, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + ), + ); + await tester.pumpAndSettle(); + + await tester.longPress(find.byType(ProductTile)); + await tester.pumpAndSettle(); + + expect(find.text('Edit product'), findsOneWidget); + + router.pop(); + await tester.pumpAndSettle(); + + expect(find.byType(ProductTile), findsOneWidget); + expect(find.text('Edit product'), findsNothing); + }); +}