Flutter navigation push, while keeping the same Appbar - flutter

I'm currently building a Flutter app where I'm struggling to figure out the best way to implement navigation.
I have 2 pages which are:
HomePage: from there I want to use an IndexedStack to manage the feed.
ProfilePage: the profile page, which (graphically) shares the same AppBar and the same Drawer as the home page.
In my App the user reaches the HomePage immediately after logging in. There is no navigation involved.
From there, I now have a TextButton, which calls Navigator.of(context).pushNamed(AppRoutes.profile).
As I said, both pages share the same Appbar and Drawer, so I created a custom myScaffold.
Both pages use this scaffold.
So the behavior is correct, since after clicking the button, the ProfilePage is moved over the HomePage.
My problem is that graphically the appbar should remain the same, but when the profile page is pushed, the animation makes it clear that it is not the same app bar.
Is it possible to animate the entry of the profile page, without
animating the rebuilding of the appbar?
Or is it possible to push a route directly into the scaffold content?
As an alternative I was just thinking of writing a function which
returns the page widget to be displayed within the scaffold content.
But this kind of approach doesn't seem right to me, since there are
routes.
From the official documentation you can see from the Interactive example what I mean:
Docs
When the second route is built over the first one, a new Appbar is built over the previous one.
But what if I need the appbar to stay the same?

You can create a sub-navigator using Navigator class.
I created a routes library (routes.dart) in my current project for navigating to other screens while bottomNavigationBar is still displayed. Using the same idea, you can perform navigations while using the same AppBar.
Here's the sample codes for your scenario.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
navigatorKey: Routes.rootNavigatorKey,
initialRoute: Routes.PAGE_INITIAL,
onGenerateRoute: Routes.onGenerateRoute,
);
}
}
routes.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter2sample/pages/home_page.dart';
import 'package:flutter2sample/pages/initial_page.dart';
import 'package:flutter2sample/pages/main_page.dart';
import 'package:flutter2sample/pages/profile_page.dart';
class Routes {
Routes._();
static const String PAGE_INITIAL = '/';
static const String PAGE_MAIN = '/main';
static const String PAGE_HOME = '/home';
static const String PAGE_PROFILE = '/profile';
static final GlobalKey<NavigatorState> rootNavigatorKey =
GlobalKey<NavigatorState>();
static final GlobalKey<NavigatorState> mainNavigatorKey =
GlobalKey<NavigatorState>();
static String currentSubNavigatorInitialRoute;
static CupertinoPageRoute<Widget> onGenerateRoute(RouteSettings settings) {
Widget page;
switch (settings.name) {
case PAGE_INITIAL:
page = InitialPage();
break;
case PAGE_MAIN:
page = MainPage();
break;
case PAGE_HOME:
page = HomePage();
break;
case PAGE_PROFILE:
page = ProfilePage();
break;
}
if (settings.name == PAGE_INITIAL &&
currentSubNavigatorInitialRoute != null) {
// When current sub-navigator initial route is set,
// do not display initial route because it is already displayed.
return null;
}
return CupertinoPageRoute<Widget>(
builder: (_) {
if (currentSubNavigatorInitialRoute == settings.name) {
return WillPopScope(
onWillPop: () async => false,
child: page,
);
}
return page;
},
settings: settings,
);
}
/// [MaterialApp] navigator key.
///
///
static NavigatorState get rootNavigator => rootNavigatorKey.currentState;
/// [PAGE_MAIN] navigator key.
///
///
static NavigatorState get mainNavigator => mainNavigatorKey.currentState;
/// Navigate to screen via [CupertinoPageRoute].
///
/// If [navigator] is not set, it will use the [rootNavigator].
static void push(Widget screen, {NavigatorState navigator}) {
final CupertinoPageRoute<Widget> route = CupertinoPageRoute<Widget>(
builder: (_) => screen,
);
if (navigator != null) {
navigator.push(route);
return;
}
rootNavigator.push(route);
}
/// Navigate to route name via [CupertinoPageRoute].
///
/// If [navigator] is not set, it will use the [rootNavigator].
static void pushNamed(
String routeName, {
NavigatorState navigator,
Object arguments,
}) {
if (navigator != null) {
navigator.pushNamed(routeName, arguments: arguments);
return;
}
rootNavigator.pushNamed(routeName, arguments: arguments);
}
/// Pop current route of [navigator].
///
/// If [navigator] is not set, it will use the [rootNavigator].
static void pop<T extends Object>({
NavigatorState navigator,
T result,
}) {
if (navigator != null) {
navigator.pop(result);
return;
}
rootNavigator.pop(result);
}
}
//--------------------------------------------------------------------------------
/// A navigator widget who is a child of [MaterialApp] navigator.
///
///
class SubNavigator extends StatelessWidget {
const SubNavigator({
#required this.navigatorKey,
#required this.initialRoute,
Key key,
}) : super(key: key);
final GlobalKey<NavigatorState> navigatorKey;
final String initialRoute;
#override
Widget build(BuildContext context) {
final _SubNavigatorObserver _navigatorObserver = _SubNavigatorObserver(
initialRoute,
navigatorKey,
);
Routes.currentSubNavigatorInitialRoute = initialRoute;
return WillPopScope(
onWillPop: () async {
if (_navigatorObserver.isInitialPage) {
Routes.currentSubNavigatorInitialRoute = null;
await SystemNavigator.pop();
return true;
}
final bool canPop = navigatorKey.currentState.canPop();
if (canPop) {
navigatorKey.currentState.pop();
}
return !canPop;
},
child: Navigator(
key: navigatorKey,
observers: <NavigatorObserver>[_navigatorObserver],
initialRoute: initialRoute,
onGenerateRoute: Routes.onGenerateRoute,
),
);
}
}
//--------------------------------------------------------------------------------
/// [NavigatorObserver] of [SubNavigator] widget.
///
///
class _SubNavigatorObserver extends NavigatorObserver {
_SubNavigatorObserver(this._initialRoute, this._navigatorKey);
final String _initialRoute;
final GlobalKey<NavigatorState> _navigatorKey;
final List<String> _routeNameStack = <String>[];
bool _isInitialPage = false;
/// Flag if current route is the initial page.
///
///
bool get isInitialPage => _isInitialPage;
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
_routeNameStack.add(route.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
#override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
_routeNameStack.remove(route.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
#override
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
_routeNameStack.remove(route.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
#override
void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
_routeNameStack.remove(oldRoute.settings.name);
_routeNameStack.add(newRoute.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
}
initial_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class InitialPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Initial Page'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('This is INITIAL page'),
TextButton(
onPressed: () => Routes.pushNamed(Routes.PAGE_MAIN),
child: const Text('To Main page'),
),
],
),
),
);
}
}
main_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class MainPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Main Page'),
),
body: SubNavigator(
navigatorKey: Routes.mainNavigatorKey,
initialRoute: Routes.PAGE_HOME,
),
);
}
}
home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.yellow,
body: SafeArea(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('This is HOME page'),
TextButton(
onPressed: () => Routes.pushNamed(
Routes.PAGE_PROFILE,
navigator: Routes.mainNavigator,
),
child: const Text('To Profile page'),
),
],
),
),
),
);
}
}
profile_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class ProfilePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey,
body: SafeArea(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('This is PROFILE page'),
TextButton(
onPressed: () => Routes.pop(navigator: Routes.mainNavigator),
child: const Text('Back to Home page'),
),
],
),
),
),
);
}
}

Related

Flutter: Error says - Could not find a generator for route RouteSettings while trying to navigate to another screen

Although questions with such error messages exist in this site, none solves my problem.
I have a button and on clicking the button, I just need to go to a different screen. But when ever I tap on the screen, the error shows up.
I first setup a route in MaterialApp and then tried to navigate to that route on tapping the button. The full code and the error message are given below:
Code:
import 'livesession1to1.dart';
class NavigationService {
static GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MaterialApp(
home: CountDownTimer(),
navigatorKey: NavigationService.navigatorKey, // set property// Added by me later from prev project
// initialRoute: "/",
routes: <String, WidgetBuilder> {
'/liveSession1to1': (context) =>LiveSession1to1(),
},
)
);
}// end of main
class CountDownTimer extends StatefulWidget {
const CountDownTimer();
final String? title='';
#override
_CountDownTimerState createState() => _CountDownTimerState();
}
class _CountDownTimerState extends State<CountDownTimer> {
#override
void initState() {
super.initState();
}// end of initstate
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Live Session'),
),
body: Text('Demo Text'),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_button(title: "Go", onPressed: () =>
Navigator.of(context ,rootNavigator: true).pushNamed('/liveSession1to1', arguments: {'room_found': 123 } )
),
],
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
Widget _button({required String title, VoidCallback? onPressed}) {
return Expanded(
child: TextButton(
child: Text(
title,
style: const TextStyle(color: Colors.white),
),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: onPressed,
));
}
}
Error found:
The following assertion was thrown while handling a gesture:
Could not find a generator for route RouteSettings("/liveSession1to1", {room_found: 123}) in the _WidgetsAppState.
Make sure your root app widget has provided a way to generate
this route.
Generators for routes are searched for in the following order:
For the "/" route, the "home" property, if non-null, is used.
Otherwise, the "routes" table is used, if it has an entry for the route.
Otherwise, onGenerateRoute is called. It should return a non-null value for any valid route not handled by "home" and "routes".
Finally if all else fails onUnknownRoute is called.
Unfortunately, onUnknownRoute was not set.
So how to solve the problem ?
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
void main() {
locatorSetup();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
onGenerateRoute: generateRoute,
navigatorKey: _navService.navigatorKey,
// I don't know what your first screen is, so I'm assuming it's a Splash Screen
home: SplashScreen());
}
}
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
_navService.pushNamed(Routes.LiveSession1to1);
},
child: Text("Go to next page"),
),
));
}
}
class LiveSession1to1 extends StatefulWidget {
const LiveSession1to1({Key? key}) : super(key: key);
#override
State<LiveSession1to1> createState() => _LiveSession1to1State();
}
class _LiveSession1to1State extends State<LiveSession1to1> {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
_navService.goBack();
},
child: Text("Go to previous page"),
),
));
}
}
GetIt locator = GetIt.instance;
void locatorSetup() {
locator
.registerLazySingleton<NavigationHandler>(() => NavigationHandlerImpl());
}
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case Routes.LiveSession1to1:
return _getPageRoute(view: LiveSession1to1(), routeName: settings.name);
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}'),
),
),
);
}
}
PageRoute _getPageRoute({String? routeName, Widget? view}) {
return MaterialPageRoute(
settings: RouteSettings(
name: routeName,
),
builder: (_) => view!,
);
}
class Routes {
static const String LiveSession1to1 = "liveSession1to1";
}
abstract class NavigationHandler {
///Pushes `destinationRoute` route onto the stack
Future<dynamic>? pushNamed(String destinationRoute, {dynamic arg});
///Pushes `destinationRoute` onto stack and removes stack items until
///`lastRoute` is hit
Future<dynamic>? pushNamedAndRemoveUntil(
String destinationRoute, String lastRoute,
{dynamic arg});
///Pushes `destinationRoute` onto stack with replacement
Future<dynamic>? pushReplacementNamed(String destinationRoute, {dynamic arg});
///Pushes `destinationRoute` after popping current route off stack
Future<dynamic>? popAndPushNamed(String destinationRoute, {dynamic arg});
///Pops current route off stack
void goBack();
///Pops routes on stack until `destinationRoute` is hit
void popUntil(String destinationRoute);
///Exits app
void exitApp();
late GlobalKey<NavigatorState> navigatorKey;
}
/// Handles navigation
class NavigationHandlerImpl implements NavigationHandler {
#override
late GlobalKey<NavigatorState> navigatorKey;
/// Constructs a NavigationHandler instance
NavigationHandlerImpl({GlobalKey<NavigatorState>? navigatorKey}) {
this.navigatorKey = navigatorKey ?? GlobalKey<NavigatorState>();
}
NavigatorState? get state => navigatorKey.currentState;
#override
void exitApp() {
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
}
#override
void goBack() {
if (state != null) {
return state!.pop();
}
}
#override
Future? popAndPushNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.popAndPushNamed(destinationRoute, arguments: arg);
}
}
#override
void popUntil(String destinationRoute) {
if (state != null) {
return state!.popUntil(ModalRoute.withName(destinationRoute));
}
}
#override
Future? pushNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.pushNamed(destinationRoute, arguments: arg);
}
}
#override
Future? pushNamedAndRemoveUntil(String destinationRoute, String lastRoute,
{arg}) {
if (state != null) {
return state!.pushNamedAndRemoveUntil(
destinationRoute,
ModalRoute.withName(lastRoute),
arguments: arg,
);
}
}
#override
Future? pushReplacementNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.pushReplacementNamed(destinationRoute, arguments: arg);
}
}
}

[FLUTTER]: Programmatically change tabs in the CustomNavigator from SecondScreen to FirstScreen

I'm currently making an app with bottom navigator. And I have troubles with navigating from SecondScreen to the FirstScreen, programmatically, inside the SecondScreen file. But I have no idea how to do it. Because I can't have the access to the CustomNavigatorState part of the CustomNavigator class.
My main.dart file:
import 'package:flutter/material.dart';
import './screens/custom_navigator.dart';
void main() async {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App Name',
home: Scaffold(
body: CustomNavigator(),
),
);
}
}
My custom_navigator.dart file, which includes CustomNavigator class and _CustomNavigatorState class:
import 'package:flutter/material.dart';
import './first_second.dart';
import './second_screen.dart';
import './third_screen.dart';
import '../widgets/tab_navigator.dart';
class CustomNavigator extends StatefulWidget {
#override
State<StatefulWidget> createState() => _CustomNavigatorState();
}
class _CustomNavigatorState extends State<CustomNavigator> {
String _currentScreen = FirstScreen.route;
List<String> _screenKeys = [
FirstScreen.route,
SecondScreen.route,
ThirdScreen.route,
];
Map<String, GlobalKey<NavigatorState>> _navigatorKeys = {
FirstScreen.route: GlobalKey<NavigatorState>(),
SecondScreen.route: GlobalKey<NavigatorState>(),
ThirdScreen.route: GlobalKey<NavigatorState>(),
};
int _selectedIndex = 0;
void changeTab(String tabItem, int index) {
_selectedTab(tabItem, index);
}
void _selectedTab(String tabItem, int index) {
if (tabItem == _currentScreen) {
_navigatorKeys[tabItem].currentState.popUntil((route) => route.isFirst);
} else {
setState(() {
_currentScreen = _screenKeys[index];
_selectedIndex = index;
});
}
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final isFirstRouteInCurrentTab =
!await _navigatorKeys[_currentScreen].currentState.maybePop();
if (isFirstRouteInCurrentTab) {
if (_currentScreen != FirstScreen.route) {
_selectedTab(FirstScreen.route, 1);
return false;
}
}
return isFirstRouteInCurrentTab;
},
child: Scaffold(
resizeToAvoidBottomPadding: true,
body: Stack(
children: [
_buildOffstageNavigator(FirstScreen.route),
_buildOffstageNavigator(ScreenScreen.route),
_buildOffstageNavigator(ThirdScreen.route),
],
),
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
_selectedTab(_screenKeys[index], index);
},
currentIndex: _selectedIndex,
items: [
BottomNavigationBarItem(
label: 'First',
),
BottomNavigationBarItem(
label: 'Second',
),
BottomNavigationBarItem(
label: 'Third',
),
],
),
),
);
}
Widget _buildOffstageNavigator(String tabItem) {
return Offstage(
offstage: _currentScreen != tabItem,
child: TabNavigator(
navigatorKey: _navigatorKeys[tabItem],
tabItem: tabItem,
),
);
}
}
TabNavigator class, where the screens added.
import 'package:flutter/material.dart';
import '../screens/first_screen.dart';
import '../screens/second_screen.dart';
import '../screens/third_screen.dart';
class TabNavigator extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey;
final String tabItem;
const TabNavigator({
Key key,
this.navigatorKey,
this.tabItem,
}) : super(key: key);
#override
Widget build(BuildContext context) {
Widget child;
if (tabItem == FirstScreen.route) {
child = FirstScreen();
} else if (tabItem == SecondScreen.route) {
child = SecondScreen();
} else if (tabItem == ThirdScreen.route) {
child = ThirdScreen();
}
return Navigator(
key: navigatorKey,
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(
builder: (context) => child,
);
},
);
}
}
I tried to navigate with Navigator.push and Navigator.pushNamed, but it navigates inside SecondScreen without changing the BottomNavigationTabBars.
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => SecondScreen(),
),
);
Navigator.of(context).pushNamed(SecondScreen.route);
Also I can't use Provider, because I don't have access to _CustomNavigatorState class. Could anybody offer me any decision of the problem. Thanks.
I notice you have the nested Scaffolds, it's probably better to move your BottomNavigationBar to the outer Scaffold so you only have one Scaffold in your app. For the body of the outter Scaffold you will have your Stack
Regarding the navigator issues. The body of your app in a Stack with three offstage widgets. Only one of the widgets is visible at a time. When changing between each Offstage widget you don't actually navigate to it, all you have to do is change your _currentScreen to which ever you would like. So if you're on page one and would like to "push" to page 2 then have something like
onPressed: () {
SetState(() {
_currentScreen = FirstScreen.route;
}
}
Then when your body rebuilds from the setState it will set the FirstScreen to be onstage and all other screens to be offstage. Showing you the FirstScreen.

Flutter provider state management, logout concept

I am trying to implement custom logout solution for my application, where no matter where user currently is, once the Logout button is clicked, app will navigate back to Login page.
My idea was, that instead of listening on every component for state changes, I would have one single listener on a master component -> MyApp.
For the sake of simplicity, I have stripped down items to bare minimum. Here is how my Profile class could look like:
class Profile with ChangeNotifier {
bool _isAuthentificated = false;
bool get isAuthentificated => _isAuthentificated;
set isAuthentificated(bool newVal) {
_isAuthentificated = newVal;
notifyListeners();
}
}
Now, under Main, I have registered this provider as following:
void main() => runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => Profile(),
)
],
child: MyApp(),
),
);
And finally MyApp component:
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return Consumer<Profile>(
builder: (context, profile, _) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Color.fromARGB(255, 0, 121, 107),
accentColor: Color.fromARGB(255, 255, 87, 34),
),
home: buildBasePage(context, profile),
);
},
);
}
Widget buildBasePage(BuildContext context, Profile currentProfile) {
return !currentProfile.isAuthentificated
? LoginComponent()
: MyHomePage(title: 'Flutter Demo Home Page test');
}
}
My idea was, that as MyApp component is the master, I should be able to create a consumer, which would be notified if current user is authentificated, and would respond accordingly.
What happens is, that when I am in e.g. MyHomePage component and I click Logout() method which looks like following:
void _logout() {
Provider.of<Profile>(context, listen: false).isAuthentificated = false;
}
I would be expecting that upon changing property, the initial MyApp component would react and generate LoginPage; which is not the case. I have tried changing from Consumer to Provider.of<Profile>(context, listen: false) yet with the same result.
What do I need to do in order for this concept to work? Is it even correct to do it this way?
I mean I could surely update my Profile class in a way, that I add the following method:
logout(BuildContext context) {
_isAuthentificated = false;
Navigator.push(
context, MaterialPageRoute(builder: (context) => LoginComponent()));
}
And then simply call Provider.of<Profile>(context, listen: false).logout(), however I thought that Provider package was designed for this...or am I missing something?
Any help in respect to this matter would be more than appreciated.
I don't know why it wasn't working for you. Here is a complete example I built based on your description. It works!
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Profile with ChangeNotifier {
bool _isAuthentificated = false;
bool get isAuthentificated {
return this._isAuthentificated;
}
set isAuthentificated(bool newVal) {
this._isAuthentificated = newVal;
this.notifyListeners();
}
}
void main() {
return runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Profile>(
create: (final BuildContext context) {
return Profile();
},
)
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return Consumer<Profile>(
builder: (final BuildContext context, final Profile profile, final Widget child) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: profile.isAuthentificated ? MyHomePage() : MyLoginPage(),
);
},
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Home [Auth Protected]")),
body: Center(
child: RaisedButton(
child: const Text("Logout"),
onPressed: () {
final Profile profile = Provider.of<Profile>(context, listen: false);
profile.isAuthentificated = false;
},
),
),
);
}
}
class MyLoginPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Login")),
body: Center(
child: RaisedButton(
child: const Text("Login"),
onPressed: () {
final Profile profile = Provider.of<Profile>(context, listen: false);
profile.isAuthentificated = true;
},
),
),
);
}
}
You don't need to pass listen:false, instead simply call
Provider.of<Profile>(context).logout()
So your Profile class would look like
class Profile with ChangeNotifier {
bool isAuthentificated = false;
logout() {
isAuthentificated = false;
notifyListeners();
}
}

BottomNavigationBar doesn't show current tab when using the offStageNavigator widget

I'm trying to build a bottomNavigationBar where each tabItem will maintain its own navigation state.
I have 5 nav items: enum TabItem { top_stories, topics, special_reports, more }. For the 1st one I'm using the HomeScreen widget to display a list of articles. Each article can be clicked on to show its full contents. For the 2nd tabItem (topics), I'd like to display a different list of items but for now I'm using the TopicScreen widget to show a simple Text field.
In my NewsApp, I am using a Stack and an Offstage widget around my Navigator class for each one of the tabItems.
For the 1st tabItem everything works OK. When I click on topics though, I don't see the contents of the respective TopicScreen widget but the whole list of articles is displayed again. The weird thing is that this list seems to be created again from scratch for this tabItem. I can perfectly select a different article for each tabItem, navigate through all the bottomNavOptions and the app will "remember" my choices.
In code:
NewsApp.dart
import 'package:flutter/material.dart';
import 'package:news_app/navigation/bottom_navigation.dart';
import 'package:news_app/navigation/tab_navigator.dart';
class NewsApp extends StatefulWidget {
// NewsApp({Key key}) : super(key: key);
#override
_NewsAppState createState() => _NewsAppState();
}
class _NewsAppState extends State<NewsApp> {
/// Give a unique key to each one of the bottom navigation tab items
Map<TabItem, GlobalKey<NavigatorState>> _navigatorKeys = {
TabItem.top_stories: GlobalKey<NavigatorState>(),
TabItem.topics: GlobalKey<NavigatorState>(),
TabItem.special_reports: GlobalKey<NavigatorState>(),
TabItem.more: GlobalKey<NavigatorState>(),
};
TabItem currentTab = TabItem.top_stories;
/// This function is passed to the onTap callback upon clicking on a [tabItem].
void _selectTab(TabItem tabItem) {
setState(() {
currentTab = tabItem;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AppNews.gr'),
elevation: 0.1,
),
/// Making tab navigation stateful. Stack all tab items, fade-in the selected
/// view and fade out the rest (unselected - _currentTab != tabItem). The
/// faded out views are laid out in the widget tree but not painted and are
/// modeled with the offstage property.
body: Stack(children: <Widget>[
_buildOffStageNavigator(TabItem.top_stories),
_buildOffStageNavigator(TabItem.topics),
_buildOffStageNavigator(TabItem.special_reports),
_buildOffStageNavigator(TabItem.more)
]),
bottomNavigationBar:
BottomNavigation(currentTab: currentTab, onSelectTab: _selectTab),
);
}
/// This function wraps each [tabItem] into each own [TabNavigator]
Widget _buildOffStageNavigator(TabItem tabItem) {
return Offstage(
offstage: currentTab != tabItem,
child: TabNavigator(
navigatorKey: _navigatorKeys[tabItem],
tabItem: tabItem,
));
}
}
TabNavigator.dart
import 'package:flutter/material.dart';
import 'package:news_app/navigation/routes.dart';
import 'package:news_app/models/articles.dart';
import 'package:news_app/navigation/bottom_navigation.dart';
import 'package:news_app/screens/Home/home_screen.dart';
import 'package:news_app/screens/Detail/detail_screen.dart';
import 'package:news_app/screens/Topics/topic_screen.dart';
/// A navigator class used to perform routing and state management among different
/// [tabItem]s. Uses a unique [navigatorKey] to track the state of the
/// [TabNavigator] object across the app.
class TabNavigator extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey;
final TabItem tabItem;
TabNavigator({this.navigatorKey, this.tabItem});
/// A method used to push a detail route in a specific [context].
void _push(BuildContext context, {Article article}) {
var routeBuilder = _routeBuilder(context, specArticle: article);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => routeBuilder[Routes.detail](context)));
}
/// A method to be passed to the route generator callback (onGenerateRoute)
/// when the app is navigated to a named route.
Map<String, WidgetBuilder> _routeBuilder(BuildContext context,
{Article specArticle}) {
return {
///The home screen containing all articles('/')
Routes.home: (context) => HomeScreen(
onPush: (specArticle) => _push(context, article: specArticle),
),
///The detail screen of a specific article('/detail')
Routes.detail: (context) => DetailScreen(article: specArticle),
///The topics screen of all the available topics('/topics')
Routes.topics: (context) => TopicScreen(), /// <-- THIS DOESN'T SEEM TO WORK.
};
}
#override
Widget build(BuildContext context) {
final routeBuilders = _routeBuilder(context);
return Navigator(
key: navigatorKey,
initialRoute: Routes.home,
onGenerateRoute: (RouteSettings routeSettings) {
return MaterialPageRoute(
settings: routeSettings,
builder: (context) => routeBuilders[routeSettings.name](context));
});
}
}
BottomNavigation.dart
import 'package:flutter/material.dart';
import 'package:news_app/screens/Home/Style/home_style.dart';
/// An enum struct of all the different bottom navigation items.
enum TabItem { top_stories, topics, special_reports, more }
/// A class built on BottomNavigationBar widget used to navigate among the app's
/// [tabItem]s. Defines static const Map<[tabItem], String>s to associate a tab
/// with a material icon.
class BottomNavigation extends StatelessWidget {
final TabItem currentTab;
final ValueChanged<TabItem> onSelectTab;
static const Map<TabItem, String> tabName = {
TabItem.top_stories: 'Top Stories',
TabItem.topics: 'Topics',
TabItem.special_reports: 'Special Reports',
TabItem.more: 'More',
};
static const Map<TabItem, Icon> tabIcon = {
TabItem.top_stories: Icon(Icons.subject),
TabItem.topics: Icon(Icons.format_list_bulleted),
TabItem.special_reports: Icon(Icons.filter_none),
TabItem.more: Icon(Icons.more_horiz),
};
BottomNavigation({this.currentTab, this.onSelectTab});
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
///Fixed type is the default when there are less than four items.
///The selected item is rendered with the selectedItemColor if it's non-null,
///otherwise the theme's ThemeData.primaryColor is used.
// type: BottomNavigationBarType.shifting,
items: [
_buildItem(
TabItem.top_stories, BottomNavigation.tabIcon[TabItem.top_stories]),
_buildItem(TabItem.topics, BottomNavigation.tabIcon[TabItem.topics]),
_buildItem(TabItem.special_reports,
BottomNavigation.tabIcon[TabItem.special_reports]),
_buildItem(TabItem.more, BottomNavigation.tabIcon[TabItem.more]),
],
onTap: (index) => onSelectTab(
TabItem.values[index],
),
selectedItemColor: bottomNavBarItemsColor,
);
}
BottomNavigationBarItem _buildItem(TabItem tabItem, Icon tabIcon) {
String text = BottomNavigation.tabName[tabItem];
return BottomNavigationBarItem(
icon: tabIcon,
title: Text(text),
backgroundColor: bottomNavBarBackgroundColor);
}
}
HomeScreen.dart
import 'package:flutter/material.dart';
import 'package:news_app/models/articles.dart';
import 'package:news_app/models/tags.dart';
import 'package:news_app/screens/Home/Style/home_style.dart';
import 'package:news_app/widgets/article_card.dart';
/// The home screen widget that shows the list of [articles]
class HomeScreen extends StatefulWidget {
final ValueChanged onPush;
HomeScreen({Key key, this.onPush}) : super(key: key);
#override
_HomeScreenState createState() => _HomeScreenState(onPushCard: onPush);
}
class _HomeScreenState extends State<HomeScreen> {
List articles;
final ValueChanged onPushCard;
_HomeScreenState({this.onPushCard});
/// Dummy fetch the list of articles (will be swapped out for the api version)
#override
void initState() {
articles = getDummyArticles();
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
//TODO-me: Research if this is actually needed
child: ListView.builder(
//shrinkWrap: true, //TODO-me: Test this
itemCount: articles.length, //TODO-me: Remove to scroll infinitely
itemBuilder: (BuildContext context, int index) {
return ArticleCard(
cardElevation: articleTileElevation,
cardMargin: articleTileMargin,
cardDecoration: articleTileDecoration,
cardTilePadding: articleTilePadding,
cardTextTitleStyle: articleTileTitleStyle,
cardTextSubHeaderStyle: articleTileSubHeaderStyle,
cardArticle: articles[index],
pushCardAction: onPushCard);
}));
}
#override
void dispose() {
super.dispose();
}
}
//TODO-me: Dummy list of articles.
List getDummyArticles() {
return[Article(
title:
'Libra cryptocurrency not out to replace existing money: project head',
subHeader:
"The Facebook-conceived (FB.O) Libra cryptocurrency, to be launched by a Geneva-based association next year, is not intended to replace existing currencies, Libra Association Managing Director Bertrand Perez said on Friday.",
journalistName: 'Susan Cornwell, Makini Brice',
content:
"""The Facebook-conceived (FB.O) Libra cryptocurrency, to be launched by a Geneve.""",
timeOfPublish: "26-09-19 10:14",
tag: [
Tag(tag: 'Fintech'),
Tag(tag: 'Cryptocurrency'),
Tag(tag: 'Facebook')
])]
}
TopicScreen.dart
import 'package:flutter/material.dart';
class TopicScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Text('Hello Topics'),
),
);
}
}

Flutter: drawer becomes unavailable when pushing subviews with Navigator

Link to source on github: https://github.com/dnn1s/flutter_navigationtest
I want to accomplish the same navigation approach as Google does with the Play Store app: the drawer lists the available "root" views (in my case, view1 to view3), while any root view can have any number of subviews and its own navigation stack. The catch is: even when the user is on one of the subviews, the drawer is still accessible by using a swipe gesture, starting from the outer left of screen and going to the center - and this is not possible with my current approach. As for the Play Store app, when you tap on an app to see its details, you can either go back by tapping the arrow on the upper left OR directly invoke the drawer by swiping.
main.dart: nothing fancy
void main() => runApp(new NavigationTestApp());
class NavigationTestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Navigation test',
home: new MainPage(),
);
}
}
mainpage.dart: this view includes the drawer and its items
import 'package:flutter/material.dart';
import 'view1.dart';
import 'view2.dart';
import 'view3.dart';
/// just a wrapper class for drawer items; in my original code, these include
/// icons and other properties
class DrawerItem {
String title;
DrawerItem({this.title});
}
class MainPage extends StatefulWidget {
/// list of items in the drawer
final drawerItems = [
new DrawerItem(title: 'Item 1'),
new DrawerItem(title: 'Item 2'),
new DrawerItem(title: 'Item 3')
];
#override
State<MainPage> createState() => new MainPageState();
}
class MainPageState extends State<MainPage> {
int _selectedPageIndex = 0;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
/// dynamic title, depending on the current view
title: new Text(widget.drawerItems[_selectedPageIndex].title),
),
drawer: new Drawer(
child: new ListView(
padding: EdgeInsets.zero,
children: <Widget>[
new DrawerHeader(child: new Text('Drawer header')),
/// quick and easy way to create the items;
/// in the original
/// code, these items are built in a loop
_buildDrawerItem(0),
_buildDrawerItem(1),
_buildDrawerItem(2),
],
)
),
body: _buildCurrentPage()
);
}
Widget _buildCurrentPage() {
switch(_selectedPageIndex) {
case 0: return new View1();
case 1: return new View2();
case 2: return new View3();
}
return new Text('Invalid page index');
}
Widget _buildDrawerItem(int index) {
return new ListTile(
title: new Text(widget.drawerItems[index].title),
selected: _selectedPageIndex == index,
onTap: () => _handleSelection(index),
);
}
void _handleSelection(int index) {
setState(() {
_selectedPageIndex = index;
});
/// close the drawer
Navigator.of(context).pop();
}
}
This is the first root view:
view1.dart:
class View1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new RaisedButton(
child: new Text('Push Subview1'),
onPressed: () {
Navigator.of(context).push(new MaterialPageRoute(
builder: (context) {
return new SubView1();
}
));
},
);
}
}
subview1.dart:
import 'package:flutter/material.dart';
class SubView1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('SubView1'),
),
body: new Text('Pushed from View1')
);
}
}
I know about the Cupertino classes, but I prefer the material design. Can someone point me in the right direction on how to implement the desired behaviour?
Since they are two different pages, drawer in one page will not be available in another page. If you do want that you should create drawer in both pages. I've wrapped everything as much as i can regarding Drawer inside a separate class called drawer.dart with comments everywhere so that it would be helpful to understand.
Plugin used
scoped_model: "^0.2.0"
mainpage.dart
import 'package:commo_drawer/drawer.dart';
import 'package:flutter/material.dart';
import 'view1.dart';
import 'view2.dart';
import 'view3.dart';
MainPageState mainPageState = new MainPageState();
class MainPage extends StatefulWidget {
#override
State<MainPage> createState() => mainPageState;
}
class MainPageState extends State<MainPage> {
MyDrawer myDrawer;
#override
void initState() {
myDrawer =
new MyDrawer(shouldRebuildState: DrawerItemClick.NEED_TO_REBUILD_STATE);
myDrawer.addListener(() {
setState(() {});
});
super.initState();
}
rebuild() {
setState(() {});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(drawerItems[myDrawer.selectedPageIndex].title),
),
drawer: myDrawer.buildDrawer(context),
body: _buildCurrentPage(),
);
}
Widget _buildCurrentPage() {
switch (myDrawer.selectedPageIndex) {
case 0:
return new View1();
case 1:
return new View2();
case 2:
return new View3();
}
return new Text('Invalid page index');
}
}
subview1.dart
import 'package:commo_drawer/drawer.dart';
import 'package:flutter/material.dart';
class SubView1 extends StatelessWidget {
final MyDrawer myDrawer = new MyDrawer(shouldRebuildState: DrawerItemClick.NEED_NOT_REBUILD_STATE);
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('SubView1'),
leading: new IconButton(
icon: new Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
}),
),
body: new Text('Pushed from View1'),
drawer: myDrawer.buildDrawer(context),
);
}
}
drawer.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
int _selectedPageIndex = 0;
BuildContext _context;
class MyDrawer extends Model {
DrawerItemClick shouldRebuildState;
MyDrawer({this.shouldRebuildState});
int get selectedPageIndex => _selectedPageIndex;
Drawer buildDrawer(BuildContext context) {
_context = context;
return new Drawer(
child: new ListView(
padding: EdgeInsets.zero,
children: <Widget>[
new DrawerHeader(child: new Text('Drawer header')),
buildDrawerItem(0),
buildDrawerItem(1),
buildDrawerItem(2),
],
),
);
}
Widget buildDrawerItem(int index) {
return new ListTile(
title: new Text(drawerItems[index].title),
selected: selectedPageIndex == index,
onTap: () => _handleSelection(index),
);
}
void _handleSelection(int index) {
Navigator.of(_context).pop(); // Close drawer
if (shouldRebuildState == DrawerItemClick.NEED_TO_REBUILD_STATE) {
if (_selectedPageIndex != index) {
_selectedPageIndex = index;
notifyListeners();
}
} else {
shouldRebuildState = DrawerItemClick.NEED_TO_REBUILD_STATE;
Navigator.of(_context).pop(); // Close SubView
if (_selectedPageIndex != index) {
_selectedPageIndex = index;
notifyListeners();
}
}
}
}
final drawerItems = [
new DrawerItem(title: 'Item 1'),
new DrawerItem(title: 'Item 2'),
new DrawerItem(title: 'Item 3'),
];
class DrawerItem {
String title;
DrawerItem({this.title});
}
enum DrawerItemClick { NEED_TO_REBUILD_STATE, NEED_NOT_REBUILD_STATE }