skip to Main Content

I am new to flutter and I am trying to build a trip advisor app to expand my skills. The problem that I have encountered is that I don’t understand how to properly implement route navigation in flutter in a complex application. I am currently building the main page where the user can select a location (a country) from a list. Once the location widget is tapped, the city page that refers to the location should be pushed on the screen. I have attached an image for how the pages should look for better understanding: Locations + SubLocation Pages.

I don’t know the most logical way to make the appbar change dinamically depending on the page context, while also adding a ‘back’ button which pops the current screen on the stack, and how to keep the same bottom navigation bar, without having to rebuild it.

This is my main.dart file


void main() async {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: MainTheme,
      initialRoute: '/mainpage',
      routes: {
        '/onboard': (context) => const OnboardingPage(),
        '/signup': (context) => const AuthPage(),
        '/mainpage': (context) => const MainPage(),
        '/locations': (context) => const LocationsPage(),
        '/state-screen': (context) => StateScreen(),
      },
    );
  }
}

My main page


class MainPage extends StatefulWidget {
  const MainPage({super.key});

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  List pages = [const HomePage(), const FavoritesPage(), const LocationsPage()];

  int currentIndex = 2;

  void onTap(int index) {
    setState(
      () {
        currentIndex = index;
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        leading: Builder(
          builder: (BuildContext context) {
            return IconButton(
              icon: Image.asset(
                'assets/icons/[email protected]',
                width: 25,
                height: 14,
                // note: fix focus radius, probably image scale
              ),
              onPressed: () {
                Scaffold.of(context).openDrawer();
              },
            );
          },
        ),
        bottomOpacity: 0.0,
        elevation: 0.0,
        title: const Text(
          'LOCATION',
          style: TextStyle(color: Colors.black),
        ),
      ),
      body: pages[currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        selectedItemColor: kSecondaryColor,
        backgroundColor: Colors.white,
        unselectedItemColor: Colors.grey.withOpacity(0.5),
        unselectedFontSize: 0,
        showUnselectedLabels: false,
        showSelectedLabels: false,
        onTap: onTap,
        currentIndex: currentIndex,
        items: const [
          BottomNavigationBarItem(
            label: 'Home',
            icon: Icon(Icons.home),
          ),
          BottomNavigationBarItem(
            label: 'Favorites',
            icon: Icon(Icons.favorite),
          ),
          BottomNavigationBarItem(
            label: 'Locations',
            icon: Icon(Icons.business_center),
          ),
          BottomNavigationBarItem(
            label: 'Calendar',
            icon: Icon(Icons.calendar_month),
          ),
          BottomNavigationBarItem(
            label: 'Account',
            icon: Icon(Icons.person),
          ),
        ],
      ),
    );
  }
}

and my locations page:


class LocationsPage extends StatefulWidget {
  const LocationsPage({super.key});

  @override
  State<LocationsPage> createState() => _LocationsPageState();
}

class _LocationsPageState extends State<LocationsPage> {
  List<LocationsDemo> locationsDemo = LocationsDemo.locationList;
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Column(
        children: [
          Expanded(
            child: ListView.builder(
              primary: true,
              scrollDirection: Axis.vertical,
              itemCount: locationsDemo.length,
              itemBuilder: (BuildContext context, int index) {
                return LocationsDemoList(
                  locationsDemo: locationsDemo[index],
                  callback: () => Navigator.pushNamed(context, '/state-screen'),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

I think that my configuration is not clean, but I don’t know how I could improve it. By my logic, I should have a main page and with a Scaffold, an AppBar, the content, and the bottom navigation bar, as it is in the code that I have written. The problem is that in the Location Page widget I am pushing the State Page, which doesn’t have a scaffold widget, and it’s code is the same as Location Page. So I have to re-use the AppBar and bottom navigation bar code from the Main Page, which for me doesn’t make sense for this type of navigation.

Pushing the State Page on the whole screen is not the result that I wanted, because I thought that the Appbar, scaffold and bottom navigation bar shouldn’t be rebuild every time the user navigates to a subroute. only some of the Appbar contents should change dinamically.

Since my configuration is wrong, I think what I should do instead is to get rid of the Main Page widget alltogether, and since Location Page should be the first page shown when the user logs into the app, build the Scaffold with the app bar and bottom navigation bar inside it.

Edit: I have found a similar question :

Bottom navigation Bar with sub navigators for each tab

I will try to implement it and edit the post if I have found the solution, but basically it’s the same effect that I am trying to achieve. A navigation similar to instagram and twitter app.

2

Answers


  1. return Scaffold(
     appbar: AppBar(),
    body: SafeArea(
      child: Column(
        children: [
          Expanded(
            child: ListView.builder(
              primary: true,
              scrollDirection: Axis.vertical,
              itemCount: locationsDemo.length,
              itemBuilder: (BuildContext context, int index) {
                return LocationsDemoList(
                  locationsDemo: locationsDemo[index],
                  callback: () => Navigator.pushAndRemoveUntil(context, '/state- 
             screen'),
                );
              },
            ),
          ),
        ],
      ),
    );
    

    Wrap the safeArea with Scaffold To get AppBar and use Navigator.pushAndRemoveUntil to navigate the page and not to go back.

    Login or Signup to reply.
  2. This is an example of what you described using the qlevar_router package

    import 'package:flutter/material.dart';
    import 'package:qlevar_router/qlevar_router.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      static List<String> tabs = [
        "Locations",
        "Store",
        "Settings",
      ];
    
      static int indexOf(String name) => tabs.indexWhere((e) => e == name);
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        final routes = [
          QRoute.withChild(
            path: '/mainpage',
            builderChild: (c) => HomePage(c),
            initRoute: '/locations',
            children: [
              QRoute(
                name: tabs[0],
                path: '/locations',
                builder: () => const ListOfCities(),
                children: [
                  QRoute(
                    path: '/:city',
                    pageType: const QFadePage(),
                    builder: () => const CityWidget(),
                  ),
                ],
              ),
              QRoute(
                name: tabs[1],
                path: '/store',
                builder: () => const Tab('Store'),
              ),
              QRoute(
                name: tabs[2],
                path: '/settings',
                builder: () => const Tab('Settings'),
              ),
            ],
          ),
        ];
        return MaterialApp.router(
          routeInformationParser: const QRouteInformationParser(),
          routerDelegate: QRouterDelegate(routes, initPath: '/mainpage'),
          theme: ThemeData.light(useMaterial3: true),
        );
      }
    }
    
    class HomePage extends StatefulWidget {
      final QRouter router;
      const HomePage(this.router, {Key? key}) : super(key: key);
      @override
      State<HomePage> createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
      var currentTab = 0;
    
      @override
      void initState() {
        widget.router.navigator.addListener(_update);
        if (MyApp.indexOf(widget.router.routeName) != -1) {
          currentTab = MyApp.indexOf(widget.router.routeName);
        }
        super.initState();
      }
    
      void _update() {
        if (MyApp.indexOf(widget.router.routeName) != -1) {
          currentTab = MyApp.indexOf(widget.router.routeName);
        }
        setState(() {});
      }
    
      @override
      void dispose() {
        widget.router.navigator.removeListener(_update);
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        var appBarTitle = MyApp.tabs[currentTab];
        if (QR.params['city'] != null) {
          appBarTitle = QR.params['city'].toString();
        }
        return Scaffold(
          appBar: AppBar(
            leading: MyApp.tabs.contains(appBarTitle)
                ? null
                : BackButton(onPressed: QR.back),
            title: Text(appBarTitle.toUpperCase()),
            centerTitle: true,
          ),
          body: widget.router,
          bottomNavigationBar: BottomNavigationBar(
            items: const [
              BottomNavigationBarItem(icon: Icon(Icons.home), label: 'home'),
              BottomNavigationBarItem(icon: Icon(Icons.store), label: 'store'),
              BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Settings')
            ],
            currentIndex: currentTab,
            onTap: (v) => QR.toName(MyApp.tabs[v]),
          ),
        );
      }
    }
    
    class Tab extends StatelessWidget {
      final String name;
      const Tab(this.name, {Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) => Center(
            child: Text(name, style: const TextStyle(fontSize: 20)),
          );
    }
    
    const _citiesNames = ['London', 'Paris', 'New York', 'Tokyo'];
    
    class ListOfCities extends StatelessWidget {
      const ListOfCities({super.key});
    
      @override
      Widget build(BuildContext context) {
        return ListView(
          children: _citiesNames
              .map((city) => ListTile(
                    title: Text(city),
                    onTap: () => QR.to('/mainpage/locations/$city'),
                  ))
              .toList(),
        );
      }
    }
    
    class CityWidget extends StatelessWidget {
      const CityWidget({super.key});
    
      @override
      Widget build(BuildContext context) {
        final city = QR.params['city'].toString();
        return Center(
          child: Text(city),
        );
      }
    }
    
    

    The application has a home page (HomePage) that contains a bottom navigation bar with three tabs: "Locations", "Store", and "Settings" and the "Locations" tab contains a list of cities that can be selected, and when a city is selected, the application navigates to a page that displays information about the selected city. The city information is displayed using the CityWidget widget.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search