Flutter navigation push, while keeping the same Appbar

Viewed 1475

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?

1 Answers

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