Exploring Material Navigation in Flutter - Custom Material Navigation class

Navigation is an essential aspect of mobile app development, allowing users to move between different screens and sections of an application. In Flutter, the Material navigation package provides a set of tools and widgets to implement navigation flows easily. In this blog post, we will explore how to use the Material navigation package to create a multi-screen Flutter app.

Custom material route class


Folder Structure

Before diving into the code, let's first set up a basic folder structure for our Flutter project. Here's an example of how you can organize your project files:

- lib
  - main.dart
  - screens
    - home_screen.dart
    - second_screen.dart
    - third_screen.dart
    - fourth_screen.dart
  - services
    - navigation_service.dart

In this structure, the main.dart file represents the entry point of our application. The screens folder contains individual screen files, and the services folder contains a navigation_service.dart file, which handles navigation-related operations.

Follw_me_linkedin


Getting Started

To start, we need to import the required dependencies. In this case, we only need the `flutter/material.dart` package, which provides the necessary widgets for building our app.

import 'package:flutter/material.dart';

We also need to define our `main()` function, which serves as the entry point of our application. Inside this function, we will call `runApp()` and pass an instance of our `MyApp` widget.

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

Creating the MyApp Widget

Our app's main widget, `MyApp`, extends `StatelessWidget` and represents the root of our application. Inside the `build()` method, we return a `MaterialApp` widget, which provides a set of configurations for our app, including navigation.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: NavigationService.navigatorKey,
      initialRoute: '/',
      routes: {
        '/': (context) => HomeScreen(),
        '/fourth': (context) => FourthScreen(),
      },
    );
  }
}

In the `MaterialApp` widget, we set the `navigatorKey` property to `NavigationService.navigatorKey`. This key is necessary to enable navigation operations across the app. We also set the `initialRoute` to `'/'`, which represents the default route when the app starts.

The `routes` property defines a map of route names and corresponding widget builders. In this example, we have two routes: `'/'` maps to the `HomeScreen` widget, and `'/fourth'` maps to the `FourthScreen` widget.

Building the HomeScreen Widget

Let's start building our screens. The `HomeScreen` widget represents the initial screen of our app. It extends `StatelessWidget`, and its `build()` method returns a `Scaffold` widget.

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                NavigationService.push(SecondScreen());
              },
              child: const Text('Push to Second Screen (Material)'),
            ),
            ElevatedButton(
              onPressed: () {
                NavigationService.pushReplacement(ThirdScreen());
              },
              child: const Text('Push Replacement to Third Screen (Material)'),
            ),
            ElevatedButton(
              onPressed: () {
                NavigationService.pushNamed('/fourth', arguments: "Testing");
              },
              child: const Text('Push Named to Fourth Screen'),
            ),
          ],
        ),
      ),
    );
  }
}

In the `build()` method of `HomeScreen`, we create a `Scaffold` widget that provides a basic app layout. It contains an `AppBar` widget with the title "Home" and a `body` that consists of a `Center` widget.

Inside the `Center` widget, we have a `Column` containing three `ElevatedButton` widgets. These buttons demonstrate different navigation actions. The first button calls `NavigationService.push()` with `SecondScreen()` as the argument, which pushes the `SecondScreen` onto the navigation stack. The second button calls `NavigationService.pushReplacement()` with `ThirdScreen()`, replacing the current screen with `ThirdScreen`. The third button uses `NavigationService.pushNamed()` to navigate to the `'fourth'` route and passes an argument of "Testing".

Building the SecondScreen Widget

The `SecondScreen` widget represents the second screen of our app. Similar to the `HomeScreen`, it extends `StatelessWidget` and returns a `Scaffold` widget in the `build()` method.

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            NavigationService.pop();
          },
          child: const Text('Pop'),
        ),
      ),
    );
  }
}

In the `build()` method of `SecondScreen`, we create a `Scaffold` widget with an `AppBar` containing the title "Second Screen". The `body` consists of a `Center` widget with a single `ElevatedButton`. This button calls `NavigationService.pop()` when pressed, which pops the current screen from the navigation stack and returns to the previous screen.

Building the ThirdScreen Widget

Next, let's create the `ThirdScreen` widget. It follows the same pattern as the previous screens, extending `StatelessWidget` and returning a `Scaffold` widget in the `build()` method.

class ThirdScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Third Screen'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            NavigationService.pushNamed('/fourth', arguments: "Testing");
          },
          child: const Text('Push Named to Fourth Screen'),
        ),
      ),
    );
  }
}

In the `build()` method of `ThirdScreen`, we create a `Scaffold` widget with an `AppBar` containing the title "Third Screen". The `body` consists of a `Center` widget with a single `ElevatedButton`. This button calls `NavigationService.pushNamed()` to navigate to the `'fourth'` route, just like in the `HomeScreen`.

Building the FourthScreen Widget

The last screen we'll create is the `FourthScreen`. This screen demonstrates how to retrieve arguments passed during navigation. Again, it extends `StatelessWidget` and returns a `Scaffold` widget in the `build()` method.

class FourthScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments as String?;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Fourth Screen'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Fourth Screen'),
            const SizedBox(height: 16),
            Text('Arguments: $args'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                NavigationService.popUntil((route) => route.isFirst);
              },
              child: const Text('Pop Until First Screen'),
            ),
          ],
        ),
      ),
    );
  }
}

In the `build()` method of `FourthScreen`, we first retrieve the arguments passed during navigation using `ModalRoute.of(context)!.settings.arguments`. In this example, we assume the argument is of type `String`. 

Next, we create a `Scaffold` widget with an `AppBar` containing the title "Fourth Screen". The `body` consists of a `Center` widget containing a `Column` with three children. The first child is a `Text` widget displaying "Fourth Screen". The second child displays the arguments received from navigation.

Finally, we have an `ElevatedButton` that calls `NavigationService.popUntil()` when pressed. This function pops all screens from the navigation stack until it reaches the first screen, effectively returning the user to the initial screen.

Creating the NavigationService

To handle navigation operations, we create a separate `NavigationService` class. This class contains static methods for pushing, popping, and manipulating the navigation stack. Here's the code for the `NavigationService` class:

class NavigationService {
  static final GlobalKey<NavigatorState> navigatorKey =
      GlobalKey<NavigatorState>();

  static Future<T?> push<T>(Widget route) {
    return navigatorKey.currentState!.push<T>(
      MaterialPageRoute(builder: (context) => route),
    );
  }

  static Future<void> pushReplacement(Widget route) {
    return navigatorKey.currentState!.pushReplacement(
      MaterialPageRoute(builder: (context) => route),
    );
  }

  static void pop<T extends Object>([T? result]) {
    return navigatorKey.currentState!.pop<T>(result);
  }

  static Future<T?> pushNamed<T>(String routeName, {Object? arguments}) {
    return navigatorKey.currentState!
        .pushNamed<T>(routeName, arguments: arguments);
  }

  static Future<void> pushReplacementNamed(String routeName,
      {Object? arguments}) {
    return navigatorKey.currentState!
        .pushReplacementNamed(routeName, arguments: arguments);
  }

  static Future<T?> popAndPushNamed<T extends Object, TO extends Object>(
      String routeName,
      {TO? result,
      Object? arguments}) {
    return navigatorKey.currentState!.popAndPushNamed<T, TO>(routeName,
        result: result, arguments: arguments);
  }

  static bool canPop() {
    return navigatorKey.currentState!.canPop();
  }

  static void popUntil(RoutePredicate predicate) {
    navigatorKey.currentState!.popUntil(predicate);
  }
}

In this class, we define a `navigatorKey` as a `GlobalKey<NavigatorState>`. This key is used to access the navigator state and perform navigation operations.

The `NavigationService` class provides static methods for common navigation operations:

  • `push`: Pushes a new screen onto the navigation stack.
  • `pushReplacement`: Replaces the current screen with a new one.
  • `pop`: Pops the current screen from the navigation stack.
  • `pushNamed`: Navigates to a named route.
  • `pushReplacementNamed`: Replaces the current screen with a named route.
  • `popAndPushNamed`: Pops the current screen and navigates to a named route.
  • `canPop`: Checks if it's possible to perform a pop operation.
  • `popUntil`: Pops screens from the navigation stack until a given predicate is true.

These methods utilize the `navigatorKey` to access the `NavigatorState` and perform the desired navigation actions.

And that's it! We have successfully implemented Material navigation in Flutter using the provided code and folder structure.

To use this navigation system in your app, make sure to import the necessary dependencies and set up the folder structure as described. Then, you can create your screens by extending `StatelessWidget` and implement the desired navigation flows using the `NavigationService` class.

Remember that this code represents a basic implementation of Material navigation and can be expanded and customized to fit your specific app requirements. You can add more screens, implement additional navigation methods, and style the UI according to your design.

Happy coding!

Comments

Popular posts from this blog

Error Handling in Flutter - Gradle issue

How to Make a Dynamic and Trending ListView with Flutter Widgets?

Understanding API integration with Getx State management