Skip to content

Commit

Permalink
feat: #681 Stateful tabs (#703)
Browse files Browse the repository at this point in the history
* feat: first working stateful tab

* feat: enum instead of String

* Update tab_navigator.dart

* feat: Working camera disable for other tabs

* fix: #640 stop camera when navigating further in the stack

* doc + null safety

* fix: activating scanner when rebuilding from ground up

* Updated goldens

The navbar got removed from the images because it is now not directly added by the pages anymore

* doc:

* Minor changes

* fix: Updating product with multiple navigators

* Minor fixes

* Simplification

* Simplification

* Simplification

* Update main.dart

* Update product_list_page.dart

Co-authored-by: monsieurtanuki <[email protected]>
  • Loading branch information
M123-dev and monsieurtanuki authored Dec 11, 2021
1 parent 692c817 commit ca3739e
Show file tree
Hide file tree
Showing 18 changed files with 201 additions and 113 deletions.
11 changes: 9 additions & 2 deletions packages/smooth_app/lib/data_models/continuous_scan_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class ContinuousScanModel with ChangeNotifier {
final String languageCode;
final String countryCode;

QRViewController? _qrViewController;

bool get hasMoreThanOneProduct => getBarcodes().length > 1;
ProductList get productList => _productList;

Expand Down Expand Up @@ -92,8 +94,13 @@ class ContinuousScanModel with ChangeNotifier {

Product getProduct(final String barcode) => _productList.getProduct(barcode);

void setupScanner(QRViewController controller) =>
controller.scannedDataStream.listen((Barcode barcode) => onScan(barcode));
void setupScanner(QRViewController controller) {
_qrViewController = controller;
controller.scannedDataStream.listen((Barcode barcode) => onScan(barcode));
}

//Used when navigating away from the QRView itself
void stopQRView() => _qrViewController?.stopCamera();

Future<void> onScan(final Barcode barcode) async {
if (barcode.code == null) {
Expand Down
4 changes: 2 additions & 2 deletions packages/smooth_app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:smooth_app/data_models/product_preferences.dart';
import 'package:smooth_app/data_models/user_preferences.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart';
import 'package:smooth_app/pages/page_manager.dart';
import 'package:smooth_app/themes/smooth_theme.dart';
import 'package:smooth_app/themes/theme_provider.dart';

Expand Down Expand Up @@ -173,7 +173,7 @@ class SmoothAppGetLanguage extends StatelessWidget {
DefaultAssetBundle.of(context),
languageCode,
);
return SmoothBottomNavigationBar.getDefaultPage();
return PageManager();
}

Future<void> _refresh(
Expand Down
110 changes: 110 additions & 0 deletions packages/smooth_app/lib/pages/page_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:smooth_app/widgets/tab_navigator.dart';

enum BottomNavigationTab {
Profile,
Scan,
History,
}

/// Here the different tabs in the bottom navigation bar are taken care of,
/// so that they are stateful, that is not only things like the scroll position
/// but also keeping the navigation on the different tabs.
///
/// Scan Page is an exception here as it needs a little more work so that the
/// camera is not kept unnecessarily kept active.
class PageManager extends StatefulWidget {
@override
State<StatefulWidget> createState() => PageManagerState();
}

class PageManagerState extends State<PageManager> {
static const List<BottomNavigationTab> _pageKeys = <BottomNavigationTab>[
BottomNavigationTab.Profile,
BottomNavigationTab.Scan,
BottomNavigationTab.History,
];

final Map<BottomNavigationTab, GlobalKey<NavigatorState>> _navigatorKeys =
<BottomNavigationTab, GlobalKey<NavigatorState>>{
BottomNavigationTab.Profile: GlobalKey<NavigatorState>(),
BottomNavigationTab.Scan: GlobalKey<NavigatorState>(),
BottomNavigationTab.History: GlobalKey<NavigatorState>(),
};

BottomNavigationTab _currentPage = BottomNavigationTab.Scan;

void _selectTab(BottomNavigationTab tabItem, int index) {
if (tabItem == _currentPage) {
_navigatorKeys[tabItem]!
.currentState!
.popUntil((Route<dynamic> route) => route.isFirst);
} else {
setState(() {
_currentPage = _pageKeys[index];
});
}
}

@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final bool isFirstRouteInCurrentTab =
!await _navigatorKeys[_currentPage]!.currentState!.maybePop();
if (isFirstRouteInCurrentTab) {
if (_currentPage != BottomNavigationTab.Scan) {
_selectTab(BottomNavigationTab.Scan, 1);
return false;
}
}
// let system handle back button if we're on the first route
return isFirstRouteInCurrentTab;
},
child: Scaffold(
body: Stack(children: <Widget>[
_buildOffstageNavigator(BottomNavigationTab.Profile),
_buildOffstageNavigator(BottomNavigationTab.Scan),
_buildOffstageNavigator(BottomNavigationTab.History),
]),
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
selectedItemColor: Colors.white,
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
onTap: (int index) {
_selectTab(_pageKeys[index], index);
},
currentIndex: _currentPage.index,
items: const <BottomNavigationBarItem>[
// TODO(M123): Translate
BottomNavigationBarItem(
icon: Icon(Icons.account_circle),
label: 'Profile',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Scan or Search',
),
BottomNavigationBarItem(
icon: Icon(Icons.history),
label: 'History',
),
],
),
),
);
}

Widget _buildOffstageNavigator(BottomNavigationTab tabItem) {
final bool offstage = _currentPage != tabItem;
return Offstage(
offstage: offstage,
child: TabNavigator(
offstage: offstage,
navigatorKey: _navigatorKeys[tabItem]!,
tabItem: tabItem,
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ class ProductDialogHelper {
return;
}
_popEd = true;
Navigator.pop(context, fetchedProduct);
// Here we use the root navigator so that we can pop dialog while using multiple navigators.
Navigator.of(context, rootNavigator: true).pop(fetchedProduct);
}

Widget _getSearchingDialog() => SmoothAlertDialog(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'package:smooth_app/pages/personalized_ranking_page.dart';
import 'package:smooth_app/pages/product/common/product_list_dialog_helper.dart';
import 'package:smooth_app/pages/product/common/product_list_item_simple.dart';
import 'package:smooth_app/pages/product/common/product_query_page_helper.dart';
import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart';

class ProductListPage extends StatefulWidget {
const ProductListPage(this.productList);
Expand Down Expand Up @@ -47,9 +46,6 @@ class _ProductListPageState extends State<ProductListPage> {
dismissible = false;
}
return Scaffold(
bottomNavigationBar: const SmoothBottomNavigationBar(
tab: SmoothBottomNavigationTab.History,
),
appBar: AppBar(
title: Row(
children: <Widget>[
Expand Down
2 changes: 0 additions & 2 deletions packages/smooth_app/lib/pages/product/new_product_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import 'package:smooth_app/helpers/launch_url_helper.dart';
import 'package:smooth_app/helpers/product_cards_helper.dart';
import 'package:smooth_app/pages/product/common/product_dialog_helper.dart';
import 'package:smooth_app/pages/product/summary_card.dart';
import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart';
import 'package:smooth_app/themes/smooth_theme.dart';
import 'package:smooth_app/themes/theme_provider.dart';
import 'package:smooth_ui_library/util/ui_helpers.dart';
Expand Down Expand Up @@ -54,7 +53,6 @@ class _ProductPageState extends State<NewProductPage> {
final MaterialColor materialColor =
SmoothTheme.getMaterialColor(themeProvider);
return Scaffold(
bottomNavigationBar: const SmoothBottomNavigationBar(),
backgroundColor: SmoothTheme.getColor(
colorScheme,
materialColor,
Expand Down
2 changes: 0 additions & 2 deletions packages/smooth_app/lib/pages/product/product_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import 'package:smooth_app/helpers/product_translation_helper.dart';
import 'package:smooth_app/pages/product/common/product_dialog_helper.dart';
import 'package:smooth_app/pages/product/common/product_query_page_helper.dart';
import 'package:smooth_app/pages/product/new_product_page.dart';
import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart';
import 'package:smooth_app/pages/user_preferences_page.dart';
import 'package:smooth_app/themes/constant_icons.dart';
import 'package:smooth_app/themes/smooth_theme.dart';
Expand Down Expand Up @@ -103,7 +102,6 @@ class _ProductPageState extends State<ProductPage> {
}
}
return Scaffold(
bottomNavigationBar: const SmoothBottomNavigationBar(),
appBar: AppBar(
title: Text(_getProductName(appLocalizations)),
actions: <Widget>[
Expand Down
22 changes: 13 additions & 9 deletions packages/smooth_app/lib/pages/scan/continuous_scan_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,34 @@ import 'package:provider/provider.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:smooth_app/data_models/continuous_scan_model.dart';
import 'package:smooth_app/pages/personalized_ranking_page.dart';
import 'package:smooth_app/pages/smooth_bottom_navigation_bar.dart';
import 'package:smooth_app/widgets/smooth_product_carousel.dart';
import 'package:smooth_ui_library/smooth_ui_library.dart';
import 'package:smooth_ui_library/util/ui_helpers.dart';

class ContinuousScanPage extends StatelessWidget {
class ContinuousScanPage extends StatefulWidget {
const ContinuousScanPage();

@override
State<ContinuousScanPage> createState() => _ContinuousScanPageState();
}

class _ContinuousScanPageState extends State<ContinuousScanPage> {
final GlobalKey _scannerViewKey = GlobalKey(debugLabel: 'Barcode Scanner');
ContinuousScanModel? model;

@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final ContinuousScanModel model = context.watch<ContinuousScanModel>();
model = context.watch<ContinuousScanModel>();
final Size screenSize = MediaQuery.of(context).size;
final Size scannerSize = Size(
screenSize.width * 0.6,
screenSize.width * 0.33,
);
final double carouselHeight =
constraints.maxHeight / 1.81; // roughly 55% of the available height
final double buttonRowHeight = areButtonsRendered(model) ? 48 : 0;
final double buttonRowHeight = areButtonsRendered(model!) ? 48 : 0;
final double availableScanHeight =
constraints.maxHeight - carouselHeight - buttonRowHeight;
// Padding for the qr code scanner. This ensures the scanner has equal spacing between buttons and carousel.
Expand All @@ -34,9 +41,6 @@ class ContinuousScanPage extends StatelessWidget {
final double viewFinderBottomOffset = carouselHeight / 2.0;
return Scaffold(
appBar: AppBar(toolbarHeight: 0.0),
bottomNavigationBar: const SmoothBottomNavigationBar(
tab: SmoothBottomNavigationTab.Scan,
),
body: Stack(
children: <Widget>[
Container(
Expand Down Expand Up @@ -64,7 +68,7 @@ class ContinuousScanPage extends StatelessWidget {
cutOutBottomOffset: viewFinderBottomOffset,
),
key: _scannerViewKey,
onQRViewCreated: model.setupScanner,
onQRViewCreated: model!.setupScanner,
),
),
SmoothRevealAnimation(
Expand All @@ -91,7 +95,7 @@ class ContinuousScanPage extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
_buildButtonsRow(context, model),
_buildButtonsRow(context, model!),
const Spacer(),
SmoothProductCarousel(
showSearchCard: true,
Expand Down
29 changes: 27 additions & 2 deletions packages/smooth_app/lib/pages/scan/scan_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import 'package:smooth_app/database/product_query.dart';
import 'package:smooth_app/pages/scan/continuous_scan_page.dart';

class ScanPage extends StatefulWidget {
const ScanPage();
const ScanPage({required this.offstage, required this.navigatorKey});

final bool offstage;
final GlobalKey<NavigatorState> navigatorKey;

@override
State<ScanPage> createState() => _ScanPageState();
Expand Down Expand Up @@ -39,9 +42,31 @@ class _ScanPageState extends State<ScanPage> {
if (_model == null) {
return const Center(child: CircularProgressIndicator());
}

return ChangeNotifierProvider<ContinuousScanModel>(
create: (BuildContext context) => _model!,
child: ContinuousScanPage(),
child: Navigator(
key: widget.navigatorKey,
onGenerateRoute: (RouteSettings routeSettings) {
return MaterialPageRoute<dynamic>(
builder: (BuildContext context) => _buildChild(),
);
},
),
);
}

//This has to be build inside of the ChangeNotifierProvider to prevent the model to be disposed.
Widget _buildChild() {
//Don't build Scanner (+activate camera) when not on the Scan Tab
if (widget.offstage) {
_model?.stopQRView();
return const Center(
child: Text(
"This shouldn't be visible since only build when offstage, when you see this page send a email to [email protected]",
));
} else {
return const ContinuousScanPage();
}
}
}
Loading

0 comments on commit ca3739e

Please sign in to comment.