5

In Flutter, I want to make screens like with Fragment in android, in this my code i try to replace each screens into current screen like with Fragment.replecae in android, i used Hook and Provider and my code work fine when in click on buttons to switch between them but i can't implementing back stack, which means when i click on Back button on phone, my code should show latest screen which i stored into _backStack variable, each swtich between this screens i stored current screen index into the this variable.

how can i solve back from this stack in my sample code?

// Switch Between screens:
DashboardPage(), UserProfilePage(), SearchPage()
------------->   ------------->     ------------->
// When back from stack:
                      DashboardPage(), UserProfilePage(), SearchPage()
Exit from application <--------------  <----------------  <-----------

i used Hook and i want to implementing this action with this library features

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MultiProvider(providers: [
    Provider.value(value: StreamBackStackSupport()),
    StreamProvider<homePages>(
      create: (context) =>
          Provider.of<StreamBackStackSupport>(context, listen: false)
              .selectedPage,
    )
  ], child: StartupApplication()));
}

class StartupApplication extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BackStack Support App',
      home: MainBodyApp(),
    );
  }
}

class MainBodyApp extends HookWidget {
  final List<Widget> _fragments = [
    DashboardPage(),
    UserProfilePage(),
    SearchPage()
  ];
  List<int> _backStack = [0];
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('BackStack Screen'),
      ),
      body: WillPopScope(
        // ignore: missing_return
        onWillPop: () {
          customPop(context);
        },
        child: Container(
          child: Column(
            children: <Widget>[
              Consumer<homePages>(
                builder: (context, selectedPage, child) {
                  _currentIndex = selectedPage != null ? selectedPage.index : 0;
                  _backStack.add(_currentIndex);
                  return Expanded(child: _fragments[_currentIndex]);
                },
              ),
              Container(
                width: double.infinity,
                height: 50.0,
                padding: const EdgeInsets.symmetric(horizontal: 15.0),
                color: Colors.indigo[400],
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    RaisedButton(
                      onPressed: () => Provider.of<StreamBackStackSupport>(
                              context,
                              listen: false)
                          .switchBetweenPages(homePages.screenDashboard),
                      child: Text('Dashboard'),
                    ),
                    RaisedButton(
                      onPressed: () => Provider.of<StreamBackStackSupport>(
                              context,
                              listen: false)
                          .switchBetweenPages(homePages.screenProfile),
                      child: Text('Profile'),
                    ),
                    RaisedButton(
                      onPressed: () => Provider.of<StreamBackStackSupport>(
                              context,
                              listen: false)
                          .switchBetweenPages(homePages.screenSearch),
                      child: Text('Search'),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void navigateBack(int index) {
    useState(() => _currentIndex = index);
  }

  void customPop(BuildContext context) {
    if (_backStack.length - 1 > 0) {
      navigateBack(_backStack[_backStack.length - 1]);
    } else {
      _backStack.removeAt(_backStack.length - 1);
      Provider.of<StreamBackStackSupport>(context, listen: false)
          .switchBetweenPages(homePages.values[_backStack.length - 1]);
      Navigator.pop(context);
    }
  }
}

class UserProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(' screenProfile ...'),
    );
  }
}

class DashboardPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(' screenDashboard ...'),
    );
  }
}

class SearchPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(' screenSearch ...'),
    );
  }
}

enum homePages { screenDashboard, screenProfile, screenSearch }

class StreamBackStackSupport {
  final StreamController<homePages> _homePages = StreamController<homePages>();

  Stream<homePages> get selectedPage => _homePages.stream;

  void switchBetweenPages(homePages selectedPage) {
    _homePages.add(homePages.values[selectedPage.index]);
  }

  void close() {
    _homePages.close();
  }
}

creativecreatorormaybenot
  • 114,516
  • 58
  • 291
  • 402
DolDurma
  • 15,753
  • 51
  • 198
  • 377

1 Answers1

18

TL;DR

The full code is at the end.

Use Navigator instead

You should approach this problem differently. I could present you with a solution that would work with your approach, however, I think that you should instead solve this by implementing a custom Navigator as this is a built-in solution in Flutter.


When you are using a Navigator, you do not need any of your stream-based management, i.e. you can remove StreamBackStackSupport entirely.

Now, you insert a Navigator widget where you had your Consumer before:

children: <Widget>[
  Expanded(
    child: Navigator(
      ...
    ),
  ),
  Container(...), // Your bottom bar..
]

The navigator manages its routes using strings, which means that we will need to have a way to convert your enum (which I renamed to Page) to Strings. We can use describeEnum for that and put that into an extension:

enum Page { screenDashboard, screenProfile, screenSearch }

extension on Page {
  String get route => describeEnum(this);
}

Now, you can get the string representation of a page using e.g. Page.screenDashboard.route.

Furthermore, you want to map your actual pages to your fragment widgets, which you can do like this:

class MainBodyApp extends HookWidget {
  final Map<Page, Widget> _fragments = {
    Page.screenDashboard: DashboardPage(),
    Page.screenProfile: UserProfilePage(),
    Page.screenSearch: SearchPage(),
  };
  ...

To access the Navigator, we need to have a GlobalKey. Usually we would have a StatefulWidget and manage the GlobalKey like that. Since you want to use flutter_hooks, I opted to use a GlobalObjectKey instead:

  @override
  Widget build(BuildContext context) {
    final navigatorKey = GlobalObjectKey<NavigatorState>(context);
  ...

Now, you can use navigatorKey.currentState anywhere in your widget to access this custom navigator. The full Navigator setup looks like this:

Navigator(
  key: navigatorKey,
  initialRoute: Page.screenDashboard.route,
  onGenerateRoute: (settings) {
    final pageName = settings.name;

    final page = _fragments.keys.firstWhere((element) => describeEnum(element) == pageName);

    return MaterialPageRoute(settings: settings, builder: (context) => _fragments[page]);
  },
)

As you can see, we pass the navigatorKey created before and define an initialRoute, making use of the route extension we created. In onGenerateRoute, we find the Page enum entry corresponding to the route name (a String) and then return a MaterialPageRoute with the appropriate _fragments entry.

To push a new route, you simply use the navigatorKey and pushNamed:

onPressed: () => navigatorKey.currentState.pushNamed(Page.screenDashboard.route),

Back button

We also need to customly call pop on our custom navigator. For this purpose, a WillPopScope is needed:

WillPopScope(
  onWillPop: () async {
    if (navigatorKey.currentState.canPop()) {
      navigatorKey.currentState.pop();
      return false;
    }

    return true;
  },
  child: ..,
)

Access the custom navigator inside of the nested pages

In any page that is passed to onGenerateRoute, i.e. in any of your "fragments", you can just call Navigator.of(context) instead of using the global key. This is possible because these routes are children of the custom navigator and thus, the BuildContext contains that custom navigator.

For example:

// In SearchPage
Navigator.of(context).pushNamed(Page.screenProfile.route);

Default navigator

You might be wondering how you can get access to the MaterialApp root navigator now, e.g. to push a new full screen route. You can use findRootAncestorStateOfType for that:

context.findRootAncestorStateOfType<NavigatorState>().push(..);

or simply

Navigator.of(context, rootNavigator: true).push(..);

Here is the full code:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(StartupApplication());
}

enum Page { screenDashboard, screenProfile, screenSearch }

extension on Page {
  String get route => describeEnum(this);
}

class StartupApplication extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BackStack Support App',
      home: MainBodyApp(),
    );
  }
}

class MainBodyApp extends HookWidget {
  final Map<Page, Widget> _fragments = {
    Page.screenDashboard: DashboardPage(),
    Page.screenProfile: UserProfilePage(),
    Page.screenSearch: SearchPage(),
  };

  @override
  Widget build(BuildContext context) {
    final navigatorKey = GlobalObjectKey<NavigatorState>(context);

    return WillPopScope(
      onWillPop: () async {
        if (navigatorKey.currentState.canPop()) {
          navigatorKey.currentState.pop();
          return false;
        }

        return true;
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text('BackStack Screen'),
        ),
        body: Container(
          child: Column(
            children: <Widget>[
              Expanded(
                child: Navigator(
                  key: navigatorKey,
                  initialRoute: Page.screenDashboard.route,
                  onGenerateRoute: (settings) {
                    final pageName = settings.name;

                    final page = _fragments.keys.firstWhere(
                        (element) => describeEnum(element) == pageName);

                    return MaterialPageRoute(settings: settings,
                        builder: (context) => _fragments[page]);
                  },
                ),
              ),
              Container(
                width: double.infinity,
                height: 50.0,
                padding: const EdgeInsets.symmetric(horizontal: 15.0),
                color: Colors.indigo[400],
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    RaisedButton(
                      onPressed: () => navigatorKey.currentState
                          .pushNamed(Page.screenDashboard.route),
                      child: Text('Dashboard'),
                    ),
                    RaisedButton(
                      onPressed: () => navigatorKey.currentState
                          .pushNamed(Page.screenProfile.route),
                      child: Text('Profile'),
                    ),
                    RaisedButton(
                      onPressed: () => navigatorKey.currentState
                          .pushNamed(Page.screenSearch.route),
                      child: Text('Search'),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class UserProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(' screenProfile ...'),
    );
  }
}

class DashboardPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(' screenDashboard ...'),
    );
  }
}

class SearchPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(' screenSearch ...'),
    );
  }
}
creativecreatorormaybenot
  • 114,516
  • 58
  • 291
  • 402
  • Awesome, thanks a lot. Now how can I use `navigatorKey.currentState .pushNamed(Page.screenDashboard.route)` in nested pages? For example `SearchPage` ? I used `Provider` for this action – DolDurma Mar 30 '20 at 21:48
  • 1
    @DolDurma Sorry, I forgot to mention. In any page that is controlled by your custom navigator, you can use `Navigator.of(context)` because that `BuildContext` contains the custom navigator. I edited the answer to include this. Glad I was able to help you. – creativecreatorormaybenot Mar 30 '20 at 21:56
  • Could you update your your post for helping anybody want to use this solution? Thanks – DolDurma Mar 30 '20 at 22:08
  • 1
    @DolDurma Yes! I did that (: See the "Acces the custom navigator inside of the nested pages" section. – creativecreatorormaybenot Mar 30 '20 at 22:34
  • your code and your solution is what i want to have, thanks a lot. i have some question. how can i avoid navigate multiple the same pages? for example when i on dashboard page clicking any other button should be couldn't navigate again and my another question is can i have multiple nested this navigation strategy? for example dashboard page can be have another this navigation, like with instagram – DolDurma Mar 31 '20 at 04:03
  • and when i separate `extension` to another file `extension RouteExtensions on Page { String get route => describeEnum(this); }` route doesn't work for me `Navigator.of(context).pushNamed(Page.screenProfile.route);` and i get this error: `Could not find a generator for route RouteSettings("screenProfile", null) in the _WidgetsAppState.` – DolDurma Mar 31 '20 at 06:10
  • @DolDurma If you want to use the extension in another file, you have to give it a name, like `extension PageExtension on Page {}`. – creativecreatorormaybenot Mar 31 '20 at 06:13
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/210632/discussion-between-doldurma-and-creativecreatorormaybenot). – DolDurma Mar 31 '20 at 06:14
  • Now this is what I call a "Quality Answer", great job, you deserve 200 :) However, there is a mistake, It should be `findRootAncestorStateOfType()` and then there is a freeze in exiting application. – CopsOnRoad Mar 31 '20 at 07:09
  • @CopsOnRoad Thanks for the edit (: I try to do my best - you learned a lot as well over time. – creativecreatorormaybenot Mar 31 '20 at 07:35
  • @creativecreatorormaybenot Thanks sir for your compliment, I knew something like that is gonna come, however I am still having issues in exiting the app (`SystemNavigator` seems to work) but these two fail, any other solution? – CopsOnRoad Mar 31 '20 at 07:36
  • @CopsOnRoad Yes, you are right. I was sloppy in that part of my answer. I am not sure why I named that example as this is obviously not how the last route of the navigator works (I said exit the app but meant pop the last route, which you would not want to do). I edited the answer to reflect this. – creativecreatorormaybenot Mar 31 '20 at 07:52
  • @ i used `get_it` to share `navigatorKey` whole application for example: `class NavigationService { final GlobalKey navigatorKey = GlobalKey(); }` and when i use `if (ModalRoute.of(navigatorKey.currentContext).settings.name != Page.screenSearch) ` i get `/` always – DolDurma Mar 31 '20 at 07:54
  • @DolDurma Yes, of course. Make sure to pass `settings` in `onGenerateRoute` and then, sorry again. The `navigatorKey` context does obviously not include the correct route settings, only the children of the custom navigator. This means that you need the context of your pages, which is not that easy to obtain. I would instead create a global variable that saves what the current screen is (you do not need to maintain a stack as only the current one matters). So if you want to share it throughout your whole app, store a `Page` global variable with the current page and update that in `onPressed`. – creativecreatorormaybenot Mar 31 '20 at 08:01
  • @creativecreatorormaybenot do you know why i get this error: `Could not find a generator for route RouteSettings("screenProfile", null) in the _WidgetsAppState. ` in our main project – DolDurma Mar 31 '20 at 09:03
  • @DolDurma This probably means that you have either not specified `onGenerateRoute` in your custom navigator *or* you are *not using* your custom navigator and instead the `MaterialApp` navigator. This happens when the `context` does not contain your custom navigator, e.g. when you call `Navigator.of(context)` in your `MainBodyApp`. – creativecreatorormaybenot Mar 31 '20 at 09:30
  • @creativecreatorormaybenot problem is when i try to separate classes to another files i can't use `Navigator.of(context).pushNamed(...)` or `Navigator.of(context, rootNavigator: true).` or `context.findRootAncestorStateOfType().push(..);` [GITHUB link](https://github.com/MahdiPishguy/nested-page-sampl), the source of your implementation – DolDurma Mar 31 '20 at 11:42
  • @DolDurma You need to pass your `navigatorKey` to your `TestBottomBar`, i.e. as a parameter. Then you can use `navigatorKey.currentState` in your `TestBottomBar` again. – creativecreatorormaybenot Mar 31 '20 at 13:29
  • @creativecreatorormaybenot this is not good idea, because of i have more that 10 pages in whole application which i want to use this feature – DolDurma Mar 31 '20 at 13:52
  • @DolDurma In the pages you can use `Navigator.of(context)` (when they are children of the custom navigator). In other widgets, such as the bottom bar, you will have to pass the global key. – creativecreatorormaybenot Mar 31 '20 at 13:54
  • @creativecreatorormaybenot let me to check that by DI – DolDurma Mar 31 '20 at 13:55
  • @creativecreatorormaybenot I'm not sure completely, but i think this problem solved by DI – DolDurma Mar 31 '20 at 14:26
  • @creativecreatorormaybenot i awarded bounty you Thanks – DolDurma Apr 04 '20 at 12:38
  • how do I use pushReplacement instead of pushNamed, since i don't want when I push the back button, it back to the same page? – Dung Ngo Sep 04 '20 at 09:30
  • @DolDuma: Nicely done. In your answer you said "You should approach this problem differently.". Can I bother you to share the other approach? I am guessing that approach doesn't require Flutter_Hooks? – Alagh Feb 15 '21 at 16:57
  • Nice answer, however getting an issue where `builder: (context) => _fragments[page])` has a compilation error due `The return type 'Widget?' isn't a 'Widget', as required by the closure's context`. Thoughts? – Jammo May 24 '22 at 09:06