GIfatahTH / states_rebuilder

a simple yet powerful state management technique for Flutter
494 stars 56 forks source link

Navigator 2 is ready #249

Closed GIfatahTH closed 2 years ago

GIfatahTH commented 2 years ago

And finally, navigator 2 is here.

Migration from navigator 1 to navigator 2

If your app is written with the old navigator1 api, like this:

final widget_ = MaterialApp(
      navigatorKey: RM.navigate.navigatorKey,
      onGenerateRoute: RM.navigate.onGenerateRoute(
        {
          '/': (_) => HomePage('Home'),
          '/page1': (RouteData routeData) => Route1(routeData.arguments as String),
        },
    );

All you have to do is to use MaterialApp.routerand define the InjectedNavigator object like this:

final myNavigator = RM.injectNavigator(
  routes: {
       '/': (_) => HomePage('Home'),
       '/page1': (RouteData routeData) => Route1(routeData.arguments as String),
  },
);

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: myNavigator.routeInformationParser,
      routerDelegate: myNavigator.routerDelegate,
    );
  }
}

That all, and your app will use Navigator 2 under the hood.

The InjectedNavigator

Here is a summary of what you can do with InjectedNavigator

  final InjectedNavigator myNavigator = RM.injectNavigator(
     // Define your routes map
     routes: {
       '/': (RouteData data) => Home(),
        // redirect all paths that starts with '/home' to '/' path
       '/home/*': (RouteData data) => data.redirectTo('/'),
       '/page1': (RouteData data) => Page1(),
       '/page1/page11': (RouteData data) => Page11(),
       '/page2/:id': (RouteData data) {
         // Extract path parameters from dynamic links
         final id = data.pathParams['id'];
         // Or inside Page2 you can use `context.routeData.pathParams['id']`
         return Page2(id: id);
        },
       '/page3/:kind(all|popular|favorite)': (RouteData data) {
         // Use custom regular expression
         final kind = data.pathParams['kind'];
         return Page3(kind: kind);
        },
       '/page4': (RouteData data) {
         // Extract query parameters from links
         // Ex link is `/page4?age=4`
         final age = data.queryParams['age'];
         // Or inside Page4 you can use `context.routeData.queryParams['age']`
         return Page4(age: age);
        },
        '/page5/bookId': (RouteData data) {
          // As deep link can have data out of boundary.
          try {
            final bookId = data.queryParams['bookId'];
            final book = books[int.parse(bookId)];
            return Page5(book: book);
          } catch {
            // bookId can be a non valid number or it can be greater than books length.
            // Dispay the unknownRoute widget
            return data.unknownRoute;
          }
        },
        // Using sub routes
        '/page6': (RouteData data) => RouteWidget(
              builder: (Widget routerOutlet) {
                return MyParentWidget(
                  child: routerOutlet;
                  // Or inside MyParentWidget you can use `context.routerOutlet`
                )
              },
              routes: {
                '/': (RouteData data) => Page6(),
                '/page51': (RouteData data) => Page61(),
              },
            ),
     },
   );

After defining InjectedNavigator variable and setting MaterialApp.router, your app is ready for navigation, which can be done imperatively or declaratively.

Declarative Navigation

In declarative navigation, you have access to a copy of the route stack, and you have the freedom to return a new stack the way you like.

myNavigator.setRouteStack(
  (pages){
    // exposes a copy of the current route stack
    return [...newPagesList];
  }
)

Imperative navigation

  myNavigator.to('/page1'); // similar to RM.navigate.toNamed('/page1')
  myNavigator.toDeeply('/page1');
  myNavigator.toReplacement('/page1', argument: 'myArgument'); // Similar to RM.navigate.toReplacementNamed('/page1')
  myNavigator.toAndRemoveUntil('/page1', queryParam: {'id':'1'}); // Similar to RM.navigate.toNamedAndRemoveUntil('/page1')
  myNavigator.back(); ; // Similar to RM.navigate.back
  myNavigator.backUntil('/page1'); // Similar to RM.navigate.backUntil('/page1')

Pageless navigation

  RM.navigate.to(HomePage());
  RM.navigate.toDialog(AlertDialog( ... ));
  RM.scaffoldShow.snackbar(SnackBar( ... ));

Redirection

Route redirection can be done in two levels, at route level and in global level.

If a route is redirected in both route and global level, the route level takes the priority over the global redirection.

Route level redirection

  final InjectedNavigator myNavigator = RM.injectNavigator(
     // Define your routes map
     routes: {
       '/': (RouteData data) => data.redirectTo('/home'),
       '/home': (RouteData data) => Home(),
        // redirect all paths that starts with '/home' to '/home' path
       '/home/*': (RouteData data) => data.redirectTo('/home'),
       // redirect dynamic links
       '/books/:bookId/authors': (RouteData data) => data.redirectTo('/authors'),
       '/authors': (RouteData data) {
            // As we are redirected here from '/books/:bookId/authors' we can get the book id.
           final bookId = data.pathParam['bookId'];

            // The location we are redirected from.
            // For example `books/1/authors`
           final redirectedFrom = data.redirectedFrom.location; 
           // Or inside Authors widget we use context.routeData.redirectedFrom.location
           return Authors();
        },
     },
   );

From the route you are redirected to, you can obtain information about the route you are redirected from via RouteData.redirectedFrom field.

For example, from the route /books/:bookId/authors we are redirected to the /authors route. From the latter route we can get the bookId and display the author of the book.

Global redirection

Global redirections are done inside OnNavigate callBack.

  final myNavigator = RM.injectNavigator(
    onNavigate: (RouteData data) {
      final toLocation = data.location;
      if (toLocation != '/signInPage' && userIsNotSigned) {
        return data.redirectTo('/signInPage');
      }
      if (toLocation == '/signInPage' && userIsSigned) {
        return data.redirectTo('/homePage');
      }
      //You can also check query or path parameters
      if (data.queryParams['userId'] == '1') {
        return data.redirectTo('/superUserPage');
      }
    },
    routes: {
      '/signInPage': (RouteData data) => SignInPage(),
      '/homePage': (RouteData data) => HomePage(),
    },
  );

If the user is not signed in and if he is navigating to any page other than the sign in page, he will be redirected to the sign in page.

Form the sign in page, the user must sign in first to be able to continue.

From the /signInPage page we can get the location the user is redirected from and let him navigate to it.

Here is what may the sign in method look like:

void signIn(String name, String password) async {
  final success = await repository.signIn(name, password);
  if(success){
    // Here the user is successfully signed in.
    final locationRedirectedFrom = context.routeData.redirectedFrom?.location;

    if(locationRedirectedFrom != null){
      // If the app is redirected form any location (deep link for example), it will continue navigate to it.
      myNavigator.to(locationRedirectedFrom);
    }else{
      // If the app accesses the 'signInPage' directly without redirection, it will navigate to the home page for example.
      myNavigator.to('homePage');
    }
  }
}

onNavigate

This is a call back that fires every time a location is successfully resolved and just before navigation. You can use this callback for globule redirection.

It exposes the current state of the InjectedNavigator (RouteData).

Let's take this example:

  final myNavigator = RM.injectNavigator(
    onNavigate: (RouteData data) {
      final toLocation = data.location;
      if (toLocation == '/homePage' && userIsNotSigned) {
        return data.redirectTo('/signInPage');
      }
      if (toLocation == '/signInPage' && userIsSigned) {
        return data.redirectTo('/homePage');
      }
      //You can also check query or path parameters
      if (data.queryParams['userId'] == '1') {
        return data.redirectTo('/superUserPage');
      }
    },
    routes: {
      '/signInPage': (RouteData data) => SignInPage(),
      '/homePage': (RouteData data) => HomePage(),
    },
  );

If the app is navigating to '/homePage' and if the user is not signed yet, the app is redirected to navigate to the sign in page.

Also if the app is navigating to '/signInPage' and if the user is already signed, the app is redirected to navigate to the home screen.

onNavigateBack

This callback is fired every time a route is removed and the app navigate back. It can be used to prevent leaving a page unless data is validated.

  final myNavigator = RM.injectNavigator(
    onNavigateBack: (RouteData data) {
      final backFrom = data.location;
      if (backFrom == '/SingInFormPage' && formIsNotSaved) {
        RM.navigate.toDialog(
          AlertDialog(
            content: Text('The form is not saved yet! Do you want to exit?'),
            actions: [
              ElevatedButton(
                onPressed: () => RM.navigate.forceBack(),
                child: Text('Yes'),
              ),
              ElevatedButton(
                onPressed: () => RM.navigate.back(),
                child: Text('No'),
              ),
            ],
          ),
        );

        return false;
      }
    },
    routes: {
      '/SingInFormPage': (RouteData data) => SingInFormPage(),
      '/homePage': (RouteData data) => HomePage(),
    },
  );

Here if the app is navigating back from sign in form page and if the form is not saved yet, the back navigation is cancelled and a Dialog is popped asking for back navigation confirmation.

If the user chooses No, the app stays in the sign form page. In contrast if the user choose YES, the app is forcefully popped and both the Dialog and the sign in form page are removed from the routing stack.

And many other features;

For more details, hit this link: https://github.com/GIfatahTH/states_rebuilder/wiki/injected_navigator_api

Here is an example catalog: https://github.com/GIfatahTH/states_rebuilder/tree/master/examples/ex004_00_navigation

This Navigator 2 has already been implemented at stable version 5.2.0, please check out our pub.dev