diff --git a/lib/app.dart b/lib/app.dart deleted file mode 100644 index 83a5c02..0000000 --- a/lib/app.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nested_navigation_demo_flutter/bottom_navigation.dart'; -import 'package:nested_navigation_demo_flutter/tab_item.dart'; -import 'package:nested_navigation_demo_flutter/tab_navigator.dart'; - -class App extends StatefulWidget { - @override - State createState() => AppState(); -} - -class AppState extends State { - var _currentTab = TabItem.red; - final _navigatorKeys = { - TabItem.red: GlobalKey(), - TabItem.green: GlobalKey(), - TabItem.blue: GlobalKey(), - }; - - void _selectTab(TabItem tabItem) { - if (tabItem == _currentTab) { - // pop to first route - _navigatorKeys[tabItem]!.currentState!.popUntil((route) => route.isFirst); - } else { - setState(() => _currentTab = tabItem); - } - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - final isFirstRouteInCurrentTab = - !await _navigatorKeys[_currentTab]!.currentState!.maybePop(); - if (isFirstRouteInCurrentTab) { - // if not on the 'main' tab - if (_currentTab != TabItem.red) { - // select 'main' tab - _selectTab(TabItem.red); - // back button handled by app - return false; - } - } - // let system handle back button if we're on the first route - return isFirstRouteInCurrentTab; - }, - child: Scaffold( - body: Stack(children: [ - _buildOffstageNavigator(TabItem.red), - _buildOffstageNavigator(TabItem.green), - _buildOffstageNavigator(TabItem.blue), - ]), - bottomNavigationBar: BottomNavigation( - currentTab: _currentTab, - onSelectTab: _selectTab, - ), - ), - ); - } - - Widget _buildOffstageNavigator(TabItem tabItem) { - return Offstage( - offstage: _currentTab != tabItem, - child: TabNavigator( - navigatorKey: _navigatorKeys[tabItem], - tabItem: tabItem, - ), - ); - } -} diff --git a/lib/color_detail_page.dart b/lib/color_detail_page.dart deleted file mode 100644 index b9e2c81..0000000 --- a/lib/color_detail_page.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -class ColorDetailPage extends StatelessWidget { - ColorDetailPage( - {required this.color, required this.title, this.materialIndex: 500}); - final MaterialColor color; - final String title; - final int materialIndex; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: color, - title: Text( - '$title[$materialIndex]', - ), - ), - body: Container( - color: color[materialIndex], - ), - ); - } -} diff --git a/lib/colors_list_page.dart b/lib/colors_list_page.dart deleted file mode 100644 index 1d46d4b..0000000 --- a/lib/colors_list_page.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; - -class ColorsListPage extends StatelessWidget { - ColorsListPage({required this.color, required this.title, this.onPush}); - final MaterialColor color; - final String title; - final ValueChanged? onPush; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text( - title, - ), - backgroundColor: color, - ), - body: Container( - color: Colors.white, - child: _buildList(), - )); - } - - final List materialIndices = [ - 900, - 800, - 700, - 600, - 500, - 400, - 300, - 200, - 100, - 50 - ]; - - Widget _buildList() { - return ListView.builder( - itemCount: materialIndices.length, - itemBuilder: (BuildContext content, int index) { - int materialIndex = materialIndices[index]; - return Container( - color: color[materialIndex], - child: ListTile( - title: Text('$materialIndex', style: TextStyle(fontSize: 24.0)), - trailing: Icon(Icons.chevron_right), - onTap: () => onPush?.call(materialIndex), - ), - ); - }); - } -} diff --git a/lib/features/help/help_page.dart b/lib/features/help/help_page.dart new file mode 100644 index 0000000..88ac388 --- /dev/null +++ b/lib/features/help/help_page.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class HelpPage extends StatelessWidget { + const HelpPage({Key? key, this.onHome, this.onPayments}) : super(key: key); + final VoidCallback? onHome; + final VoidCallback? onPayments; + + @override + Widget build(BuildContext context) { + print('HelpPage rebuild'); + return Scaffold( + appBar: AppBar( + title: Text('Get help'), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: onHome, + child: Text('Go Home'), + ), + ElevatedButton( + onPressed: onPayments, + child: Text('Go to Payments'), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/home_page/home_navigator.dart b/lib/features/home_page/home_navigator.dart new file mode 100644 index 0000000..0b21a94 --- /dev/null +++ b/lib/features/home_page/home_navigator.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:nested_navigation_demo_flutter/features/home_page/home_page.dart'; +import 'package:nested_navigation_demo_flutter/features/home_page/models/transaction.dart'; +import 'package:nested_navigation_demo_flutter/features/transaction_details/transaction_details.dart'; + +class HomeNavigatorRoutes { + static const String root = '/'; + static const String detail = '/detail'; + static const String report = '/report'; +} + +class HomeNavigatorRouter { + static Route? generateRoute(RouteSettings settings) { + switch (settings.name) { + case HomeNavigatorRoutes.root: + return MaterialPageRoute(builder: (_) => HomePage()); + case HomeNavigatorRoutes.detail: + final transaction = settings.arguments as Transaction; + return MaterialPageRoute( + builder: (_) => TransactionDetails(transaction: transaction) + // fullscreenDialog: false, + ); + } + return null; + } +} + +class HomeNavigator extends StatelessWidget { + HomeNavigator({required this.navigatorKey}); + final GlobalKey? navigatorKey; + + @override + Widget build(BuildContext context) { + return Navigator( + key: navigatorKey, + // TODO: Need to change this and persist it somewhere!!! + initialRoute: HomeNavigatorRoutes.root, + onGenerateRoute: HomeNavigatorRouter.generateRoute, + ); + } +} diff --git a/lib/features/home_page/home_page.dart b/lib/features/home_page/home_page.dart new file mode 100644 index 0000000..feefafd --- /dev/null +++ b/lib/features/home_page/home_page.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:nested_navigation_demo_flutter/features/home_page/home_navigator.dart'; +import 'package:nested_navigation_demo_flutter/features/home_page/models/transaction.dart'; +import 'package:nested_navigation_demo_flutter/features/home_page/transaction_tile.dart'; + +final transactions = [ + Transaction( + data: Icons.add, title: 'Three', subtitle: 'June bill', amount: -10), + Transaction( + data: Icons.add, title: 'Unicef', subtitle: '1293786423', amount: -24), + Transaction( + data: Icons.add, + title: 'National Trust', + subtitle: 'Parking', + amount: -4.5), + Transaction( + data: Icons.add, title: 'Andrea Bizzotto', subtitle: 'Topup', amount: 10), +]; + +class HomePage extends StatelessWidget { + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + print('HomePage rebuild'); + return Scaffold( + appBar: AppBar( + title: Text('Current Account'), + actions: [ + IconButton( + icon: Icon(Icons.pie_chart), + onPressed: () => Navigator.of(context, rootNavigator: true) + .pushNamed(HomeNavigatorRoutes.report), + ) + ], + bottom: PreferredSize( + preferredSize: Size.fromHeight(60), + child: Text('\$1,014.69'), + ), + ), + body: TransactionsListView( + transactions: transactions, + ), + ); + } +} + +class TransactionsListView extends StatelessWidget { + const TransactionsListView({Key? key, required this.transactions}) + : super(key: key); + final List transactions; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemBuilder: (context, index) { + return TransactionTile( + transaction: transactions[index], + onSelected: (transaction) => Navigator.of(context) + .pushNamed(HomeNavigatorRoutes.detail, arguments: transaction), + ); + }, + itemCount: transactions.length, + ); + } +} diff --git a/lib/features/home_page/models/transaction.dart b/lib/features/home_page/models/transaction.dart new file mode 100644 index 0000000..c89429e --- /dev/null +++ b/lib/features/home_page/models/transaction.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +class Transaction { + final String title; + final String subtitle; + final IconData data; + final double amount; + Transaction({ + required this.title, + required this.subtitle, + required this.data, + required this.amount, + }); + + Transaction copyWith({ + String? title, + String? subtitle, + IconData? data, + double? amount, + }) { + return Transaction( + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + data: data ?? this.data, + amount: amount ?? this.amount, + ); + } + + Map toMap() { + return { + 'title': title, + 'subtitle': subtitle, + 'data': data.codePoint, + 'amount': amount, + }; + } + + factory Transaction.fromMap(Map map) { + return Transaction( + title: map['title'], + subtitle: map['subtitle'], + data: IconData(map['data'], fontFamily: 'MaterialIcons'), + amount: map['amount'], + ); + } + + String toJson() => json.encode(toMap()); + + factory Transaction.fromJson(String source) => + Transaction.fromMap(json.decode(source)); + + @override + String toString() { + return 'Transaction(title: $title, subtitle: $subtitle, data: $data, amount: $amount)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is Transaction && + other.title == title && + other.subtitle == subtitle && + other.data == data && + other.amount == amount; + } + + @override + int get hashCode { + return title.hashCode ^ subtitle.hashCode ^ data.hashCode ^ amount.hashCode; + } +} diff --git a/lib/features/home_page/transaction_tile.dart b/lib/features/home_page/transaction_tile.dart new file mode 100644 index 0000000..dbd5a70 --- /dev/null +++ b/lib/features/home_page/transaction_tile.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:nested_navigation_demo_flutter/features/home_page/models/transaction.dart'; + +class TransactionTile extends StatelessWidget { + const TransactionTile({Key? key, required this.transaction, this.onSelected}) + : super(key: key); + final Transaction transaction; + final ValueChanged? onSelected; + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(transaction.data), + title: Text(transaction.title), + subtitle: Text(transaction.subtitle), + // TODO: Currency formatting + trailing: Text('\$${transaction.amount}'), + onTap: () => onSelected?.call(transaction), + ); + } +} diff --git a/lib/bottom_navigation.dart b/lib/features/main_navigation/bottom_navigation.dart similarity index 60% rename from lib/bottom_navigation.dart rename to lib/features/main_navigation/bottom_navigation.dart index e5b8c5b..6cc7b37 100644 --- a/lib/bottom_navigation.dart +++ b/lib/features/main_navigation/bottom_navigation.dart @@ -8,32 +8,29 @@ class BottomNavigation extends StatelessWidget { @override Widget build(BuildContext context) { + final selectedItemColor = Theme.of(context).primaryColor; return BottomNavigationBar( type: BottomNavigationBarType.fixed, items: [ - _buildItem(TabItem.red), - _buildItem(TabItem.green), - _buildItem(TabItem.blue), + _buildItem(TabItem.home, selectedItemColor), + _buildItem(TabItem.payments, selectedItemColor), + _buildItem(TabItem.help, selectedItemColor), ], onTap: (index) => onSelectTab( TabItem.values[index], ), currentIndex: currentTab.index, - selectedItemColor: activeTabColor[currentTab]!, + selectedItemColor: selectedItemColor, ); } - BottomNavigationBarItem _buildItem(TabItem tabItem) { + BottomNavigationBarItem _buildItem(TabItem tabItem, Color selectedItemColor) { return BottomNavigationBarItem( icon: Icon( - Icons.layers, - color: _colorTabMatching(tabItem), + tabIcon[tabItem], + color: currentTab == tabItem ? selectedItemColor : Colors.grey, ), label: tabName[tabItem], ); } - - Color _colorTabMatching(TabItem item) { - return currentTab == item ? activeTabColor[item]! : Colors.grey; - } } diff --git a/lib/features/main_navigation/main_navigation.dart b/lib/features/main_navigation/main_navigation.dart new file mode 100644 index 0000000..183b4a2 --- /dev/null +++ b/lib/features/main_navigation/main_navigation.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:nested_navigation_demo_flutter/features/help/help_page.dart'; +import 'package:nested_navigation_demo_flutter/features/home_page/home_navigator.dart'; +import 'package:nested_navigation_demo_flutter/features/main_navigation/bottom_navigation.dart'; +import 'package:nested_navigation_demo_flutter/features/payments/payments_page.dart'; +import 'package:nested_navigation_demo_flutter/tab_item.dart'; + +class MainNavigation extends StatefulWidget { + @override + State createState() => MainNavigationState(); +} + +class MainNavigationState extends State { + // Note: these variables encapsulate state that is only accessible to this AppState instance. + // If you want to share state between multiple widget instances, you have a couple of options: + // 1. Use Navigator 2.0 (https://flutter.io/docs/widgets/Navigator-2.0) + // 2. Move this state to a separate class that can be shared between widgets. + final _pageController = PageController(initialPage: 0); + var _currentTab = TabItem.home; + + // Navigator key used for nested navigation in the home page + final _homeNavigatorKey = GlobalKey(); + + void _selectTab(TabItem tabItem) { + if (tabItem == _currentTab) { + if (_currentTab == TabItem.home) { + // pop to first route + _homeNavigatorKey.currentState!.popUntil((route) => route.isFirst); + } + } else { + setState(() { + _pageController.jumpToPage(tabItem.index); + _currentTab = tabItem; + }); + } + } + + @override + Widget build(BuildContext context) { + print('MainNavigation rebuild'); + return WillPopScope( + onWillPop: () async { + if (_currentTab == TabItem.home) { + return !await _homeNavigatorKey.currentState!.maybePop(); + // if (isFirstRouteInCurrentTab) { + // // if not on the 'main' tab + // if (_currentTab != TabItem.home) { + // // select 'main' tab + // _selectTab(TabItem.home); + // // back button handled by app + // return false; + // } + // } + // let system handle back button if we're on the first route + //return isFirstRouteInCurrentTab; + } + return true; + }, + child: Scaffold( + body: PageView( + controller: _pageController, + physics: NeverScrollableScrollPhysics(), + children: [ + // TODO: Stack not preserved when tabs are switched + HomeNavigator( + navigatorKey: _homeNavigatorKey, + ), + PaymentsPage(), + HelpPage( + onHome: () => _selectTab(TabItem.home), + onPayments: () => _selectTab(TabItem.payments), + ), + ], + ), + bottomNavigationBar: BottomNavigation( + currentTab: _currentTab, + onSelectTab: _selectTab, + ), + ), + ); + } +} diff --git a/lib/features/main_navigation/main_navigator_routes.dart b/lib/features/main_navigation/main_navigator_routes.dart new file mode 100644 index 0000000..60cd17c --- /dev/null +++ b/lib/features/main_navigation/main_navigator_routes.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:nested_navigation_demo_flutter/features/home_page/home_navigator.dart'; +import 'package:nested_navigation_demo_flutter/features/main_navigation/main_navigation.dart'; +import 'package:nested_navigation_demo_flutter/features/report/report_page.dart'; + +class MainNavigatorRoutes { + static const String root = '/'; + static const String report = '/report'; +} + +class MainNavigatorRouter { + static Route? generateRoute(RouteSettings settings) { + switch (settings.name) { + case MainNavigatorRoutes.root: + return MaterialPageRoute(builder: (_) => MainNavigation()); + case HomeNavigatorRoutes.report: + return MaterialPageRoute( + builder: (_) => ReportPage(), + fullscreenDialog: true, + ); + } + return null; + } +} diff --git a/lib/features/payments/payments_page.dart b/lib/features/payments/payments_page.dart new file mode 100644 index 0000000..448b4d1 --- /dev/null +++ b/lib/features/payments/payments_page.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +// some mock data +final paymentOptions = [ + PaymentOption( + title: 'Pay someone', + subtitle: 'By bank transfer or send a link', + icon: Icons.arrow_forward, + ), + PaymentOption( + title: 'Request money', + subtitle: 'Ask someone for money you\'re owed', + icon: Icons.arrow_back, + ), + PaymentOption( + title: 'Move money', + subtitle: 'From one of your accounts to another', + icon: Icons.double_arrow_outlined, + ), +]; + +class PaymentsPage extends StatelessWidget { + const PaymentsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + print('PaymentsPage rebuild'); + return Scaffold( + appBar: AppBar( + title: Text('Pay someone'), + ), + body: PaymentOptionsListView( + paymentOptions: paymentOptions, + ), + ); + } +} + +class PaymentOptionsListView extends StatelessWidget { + const PaymentOptionsListView({Key? key, required this.paymentOptions}) + : super(key: key); + final List paymentOptions; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemBuilder: (context, index) { + return PaymentOptionTile( + paymentOptions[index], + onSelected: () => showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Not implemented!'), + ), + ), + ); + }, + itemCount: paymentOptions.length, + ); + } +} + +class PaymentOption { + PaymentOption({ + required this.title, + required this.subtitle, + required this.icon, + }); + final String title; + final String subtitle; + final IconData icon; +} + +class PaymentOptionTile extends StatelessWidget { + const PaymentOptionTile(this.option, {Key? key, this.onSelected}) + : super(key: key); + final PaymentOption option; + final VoidCallback? onSelected; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon(option.icon), + title: Text(option.title), + subtitle: Text(option.subtitle), + trailing: Icon(Icons.chevron_right), + onTap: onSelected, + ); + } +} diff --git a/lib/features/report/report_page.dart b/lib/features/report/report_page.dart new file mode 100644 index 0000000..bf6561c --- /dev/null +++ b/lib/features/report/report_page.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class ReportPage extends StatelessWidget { + const ReportPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Report'), + ), + ); + } +} diff --git a/lib/features/transaction_details/transaction_details.dart b/lib/features/transaction_details/transaction_details.dart new file mode 100644 index 0000000..3e70a34 --- /dev/null +++ b/lib/features/transaction_details/transaction_details.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:nested_navigation_demo_flutter/features/home_page/models/transaction.dart'; + +class TransactionDetails extends StatelessWidget { + const TransactionDetails({Key? key, required this.transaction}) + : super(key: key); + final Transaction transaction; + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Wednesday, 16 Jun'), + ), + body: Center( + child: Column( + children: [ + Text(transaction.title), + Text(transaction.subtitle), + Text('\$${transaction.amount}'), + ], + )), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index adc02cc..5720164 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:nested_navigation_demo_flutter/app.dart'; +import 'package:nested_navigation_demo_flutter/features/main_navigation/main_navigator_routes.dart'; void main() => runApp(MyApp()); @@ -9,9 +9,10 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( - primarySwatch: Colors.blue, + primarySwatch: Colors.indigo, ), - home: App(), + initialRoute: MainNavigatorRoutes.root, + onGenerateRoute: MainNavigatorRouter.generateRoute, ); } } diff --git a/lib/tab_item.dart b/lib/tab_item.dart index 8a174c6..9c9c573 100644 --- a/lib/tab_item.dart +++ b/lib/tab_item.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; -enum TabItem { red, green, blue } +enum TabItem { home, payments, help } const Map tabName = { - TabItem.red: 'red', - TabItem.green: 'green', - TabItem.blue: 'blue', + TabItem.home: 'Home', + TabItem.payments: 'Payments', + TabItem.help: 'Help', }; -const Map activeTabColor = { - TabItem.red: Colors.red, - TabItem.green: Colors.green, - TabItem.blue: Colors.blue, +const Map tabIcon = { + TabItem.home: Icons.home, + TabItem.payments: Icons.payment, + TabItem.help: Icons.help, }; diff --git a/lib/tab_navigator.dart b/lib/tab_navigator.dart deleted file mode 100644 index 34126e0..0000000 --- a/lib/tab_navigator.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:nested_navigation_demo_flutter/color_detail_page.dart'; -import 'package:nested_navigation_demo_flutter/colors_list_page.dart'; -import 'package:nested_navigation_demo_flutter/tab_item.dart'; - -class TabNavigatorRoutes { - static const String root = '/'; - static const String detail = '/detail'; -} - -class TabNavigator extends StatelessWidget { - TabNavigator({required this.navigatorKey, required this.tabItem}); - final GlobalKey? navigatorKey; - final TabItem tabItem; - - void _push(BuildContext context, {int materialIndex: 500}) { - var routeBuilders = _routeBuilders(context, materialIndex: materialIndex); - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - routeBuilders[TabNavigatorRoutes.detail]!(context), - ), - ); - } - - Map _routeBuilders(BuildContext context, - {int materialIndex: 500}) { - return { - TabNavigatorRoutes.root: (context) => ColorsListPage( - color: activeTabColor[tabItem]!, - title: tabName[tabItem]!, - onPush: (materialIndex) => - _push(context, materialIndex: materialIndex), - ), - TabNavigatorRoutes.detail: (context) => ColorDetailPage( - color: activeTabColor[tabItem]!, - title: tabName[tabItem]!, - materialIndex: materialIndex, - ), - }; - } - - @override - Widget build(BuildContext context) { - final routeBuilders = _routeBuilders(context); - return Navigator( - key: navigatorKey, - initialRoute: TabNavigatorRoutes.root, - onGenerateRoute: (routeSettings) { - return MaterialPageRoute( - builder: (context) => routeBuilders[routeSettings.name!]!(context), - ); - }, - ); - } -}