15. Best Way Of Navigation

15. Best Way Of Navigation

Hello, Welcome back. Glad that you’re here.

We're building a Movie App with the best coding practices and tools. In the previous tutorial, we worked on adding a local database to store favorite movies and user preferred language.

In this tutorial, we will not add any further screens as mostly all our primary features are done. From now on, we will work on features that convert an average app to a better app. Where you care about the UI feedback engaging users for a longer time and providing fun time while using the application. We can give the user a better experience by writing code that can help in scaling the app quickly with features. Another way is to give proper feedback on what is happening in the application, like showing loaders while we make an API call, etc. One more way would be to keep the plugins updated and grab most of their features.

Let's get started.

Current Implementation

We are using Navigator.of(context).push(<Widget>); to navigate between screens. This seems easy and straightforward. But, in this way we are duplicating our code like whenever you tap on any movie card, you have to navigate to the movie detail screen. There are 4 scenarios in the app currently. In the future, as and when we add more features, the instance of the code will increase.

Navigator.of(context).push(
  MaterialPageRoute(
    builder: (context) => MovieDetailScreen(
      movieDetailArguments: MovieDetailArguments(movie.id),
    ),
  ),
);

Clearly, MovieDetailScreen uses only movieDetailArguments now, but when you want to add or remove any such parameters from MovieDetailScreen you'll have to change it in all the places. So, to manage this properly and have a maintainable way of navigating to screens, let's try to keep this in one place.

Routes

First and foremost is to create a file to collect all the possible routes in the app and put their path in the constants file. Create a route_constants.dart file in constants folder:

//1
class RouteList {
  RouteList._();

  //2
  static const String initial = "/";
  static const String movieDetail = "/movie-detail";
  static const String watchTrailer = "/watch-trailer";
  static const String favorite = "/favorite";
}
  1. Create a class RouteList and give it a non-initializable constructor.
  2. Next, you'll write routes. Starting with the initial route i.e. home which is indicated by forward-slash(/) and then all the other routes with unique paths for movie detail screen, watch trailer screen, and favorite screen. We don't need for search screen because that we handle it via an inbuilt call to search screen using showSearch().

Pass Initial Route

Now that we are ready with the initial route, let's make sure that we add that in the MaterialApp. Open movie_app.dart:

//1
builder: (context, child) {
  return child;
},
//2
initialRoute: RouteList.initial,
  1. Remove the home parameter and instead pass the builder which will return in the dynamic child widget of MaterialApp. This dynamic child widget will be generated by a RouteFactory that we will create after this.
  2. Next, tell the MaterialApp what route it has to consider as the initialRoute. We will create a mapping of the routes with widgets in RouteFactory itself.

Route Factory

There are 2 main things left to implement navigation, first create a route factory and then generate a route using it. Let's first create the RouteFactory.

Create a file routes in the presentation folder:

class Routes {
  //1
  static Map<String, WidgetBuilder> getRoutes(RouteSettings settings) => {
    RouteList.initial: (context) => HomeScreen(),
    RouteList.movieDetail: (context) => MovieDetailScreen(
          movieDetailArguments: settings.arguments,
        ),
    RouteList.watchVideo: (context) => WatchVideoScreen(
          watchVideoArguments: settings.arguments,
        ),
    RouteList.favorite: (context) => FavoriteScreen(),
  };
}
  1. Create a class Routes and make a static method getRoutes(). This method will return a Map of String and WidgetBuilder, where key is the route path and value is a function that returns a widget or screens. For those screens that require arguments like MovieDetailScreen, pass the arguments from RouteSettings. You will be better able to relate when we call the pushNamed().

Generate Route

Now, we will write code where the real magic happens. Based on the route, we will return the widget with arguments whenever necessary. Open movie_app.dart and add onGenerateRoute property of MaterialApp.

//1
onGenerateRoute: (RouteSettings settings) {
  //2
  final routes = Routes.getRoutes(settings);
  //3
  WidgetBuilder builder = routes[settings.name];
  //4
  return FadePageRouteBuilder(
    builder: builder,
    settings: settings,
  );
},
  1. The onGenerateRoute property takes in a method with the RouteSettings type field. The RouteSettings type class consists of 2 things - the name of the route and arguments attached to the route. Both are which are useful to us.
  2. Next, using these settings get the Routes from the map we created before.
  3. Next, get the WidgetBuilder by the pathname from the map.
  4. Last, pass the builder and setting to FadePageRouteBuilder. This customer builder will also be useful to add some transitions while navigating between screens. Let's create that too. Although, I have already shown this many times in my videos.

FadePageRouteBuilder

Create a new file fade_page_route_builder.dart in the presentation folder:

//1
class FadePageRouteBuilder<T> extends PageRouteBuilder<T> {
  //2
  final WidgetBuilder builder;
  final RouteSettings settings;

  FadePageRouteBuilder({
    @required this.builder,
    @required this.settings,
  }) : super(
        //3
        pageBuilder: (context, animation, secondaryAnimation) =>
            builder(context),
        //4
        transitionsBuilder: (
          context,
          animation,
          secondaryAnimation,
          child,
        ) {
          var curve = Curves.ease;

          var tween =
              Tween(begin: 0.0, end: 1.0).chain(CurveTween(curve: curve));
          //5
          return FadeTransition(
            opacity: animation.drive(tween),
            child: child,
          );
        },
        //6
        transitionDuration: const Duration(milliseconds: 500),
        settings: settings,
      );
}
  1. Create a class that extends PageRouteBuilder.
  2. Define the 2 fields that are required - WidgetBuilder and RouteSettings.
  3. In the super(), return the builder itself.
  4. For transitions between the widgets, use the transitionBuilder and define Curve and Tween as per the animation you required. We are going with a faded transition.
  5. Now, return the Fadetransition with animated opacity.
  6. Lastly, define the duration for the transition to take place. And, pass the RouteSettings as well.

Use Navigator.pushNamed()

And at last, we have come to a place where we need to use the pushNamed() instead of push().

//1
Navigator.of(context).pushNamed(RouteList.favorite);

//2
Navigator.of(context).pushNamed(
  RouteList.movieDetail,
  arguments: MovieDetailArguments(movieId),
);

//3
Navigator.of(context).pushNamed(
  RouteList.watchTrailer,
  arguments: WatchVideoArguments(_videos),
);
  1. Use the RouteList.favorite to open your favorite movies screen.
  2. Use the RouteList.movieDetail to open the movie detail screen with arguments.
  3. In a similar way, use the RouteList.watchTrailer to open the trailer screen.

Run the app and do a sanity check of whether you can navigate to all screens of the screen or not. We also have a Fade effect now while navigating to the screens.

By this, we have come to an end for this tutorial. In the next tutorial, we will work on adding loader to the API calls. See you in the next tutorial. Thanks for reading.

Did you find this article valuable?

Support Prateek Sharma by becoming a sponsor. Any amount is appreciated!