APS Navigator - App Pagination System

A wrapper around Navigator 2.0 and Router/Pages to make their use a easier.

This library is just a wrapper around Navigator 2.0 and Router/Pages API that tries to make their use easier:

Basic feature set

What we've tried to achieve:

  • Simple API
  • Easy setup
  • Minimal amount of "new classes types" to learn:
    • No need to extend(or implement) anything
  • Web support (check the images in the following sections):
    • Back/Forward buttons
    • Dynamic URLs
    • Static URLs
    • Recover app state from web history
  • Control of Route Stack:
    • Add/remove Pages at a specific position
    • Add multiples Pages at once
    • Remove a range of pages at once
  • Handles Operational System events
  • Internal(Nested) Navigators

What we didn't try to achieve:

  • To use code generation
    • Don't get me wrong. Code generation is a fantastic technique that makes code clear and coding faster - we have great libraries that are reference in the community and use it
    • The thing is: It doesn't seems natural to me have to use this kind of procedure for something "basic" as navigation
  • To use Strongly-typed arguments passing

Overview

1 - Create the Navigator and define the routes:

final navigator = APSNavigator.from(
  routes: {
    '/dynamic_url_example{?tab}': DynamicURLPage.route,  
    '/': ...
  },
);

2 - Configure MaterialApp to use it:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: navigator,
      routeInformationParser: navigator.parser,
    );
  }
}

3 - Create the widget Page (route):

class DynamicURLPage extends StatefulWidget {
  final int tabIndex;
  const DynamicURLPage({Key? key, required this.tabIndex}) : super(key: key);

  @override
  _DynamicURLPageState createState() => _DynamicURLPageState();

  // Builder function
  static Page route(RouteData data) {
    final tab = data.values['tab'] == 'books' ? 0 : 1;
    return MaterialPage(
      key: const ValueKey('DynamicURLPage'), // Important! Always include a key
      child: DynamicURLPage(tabIndex: tab),
    );
  }
}
  • You don't need to use a static function as PageBuilder, but it seems to be a good way to organize things.
  • Important: AVOID using 'const' keyword at MaterialPage or DynamicURLPage levels, or Pop may not work correctly with Web History.
  • Important: Always include a Key.

4 - Navigate to it:

 APSNavigator.of(context).push(
    path: '/dynamic_url_example',
    params: {'tab': 'books'},
 );
  • The browser's address bar will display: /dynamic_url_example?tab=books.
  • The Page will be created and put at the top of the Route Stack.

The following sections describe better the above steps.

Usage

1 - Creating the Navigator and defining the Routes:

final navigator = APSNavigator.from(

  // Defines the initial route - default is '/':
  initialRoute: '/dynamic_url_example', 

  //  Defines the initial route params - default is 'const {}':
  initialParams: {'tab': '1'},

  routes: {
    // Defines the location: '/static_url_example'
    '/static_url_example': PageBuilder..,

    // Defines the location (and queries): '/dynamic_url_example?tab=(tab_value)&other=(other_value)'
    // Important: Notice that the '?' is used only once 
    '/dynamic_url_example{?tab,other}': PageBuilder..,

    // Defines the location (and path variables): '/posts' and '/posts/(post_id_value)'
    '/posts': PageBuilder..,
    '/posts/{post_id}': PageBuilder..,

    // Defines the location (with path and query variables): '/path/(id_value)?q1=(q1_value)&q2=(q2_value)'.
    '/path/{id}?{?q1,q2}': PageBuilder..,

    // Defines app root - default
    '/': PageBuilder..,
  },
);

routes is just a map between Templates and Page Builders:

  • Templates are simple strings with predefined markers to Path ({a}) and Query({?a,b,c..}) values.
  • Page Builders are plain functions that return a Page and receive a RouteData. Check the section 3 bellow.

Given the configuration above, the app will open at: /dynamic_url_example?tab=1.

2 - Configure MaterialApp:

After creating a Navigator, we need to set it up to be used:

  • Set it as MaterialApp.router.routeDelegate.

  • Remember to also add the MaterialApp.router.routeInformationParser:

    class MyApp extends StatelessWidget {
    const MyApp({Key? key}) : super(key: key);

    @override
    Widget build(BuildContext context) {
      return MaterialApp.router(
        routerDelegate: navigator,
        routeInformationParser: navigator.parser,
      );
    }
    

    }

3 - Creating the widget Page(route):

When building a Page:

  • The library tries to match the address templates with the current address. E.g.:

    • Template: /dynamic_url_example/{id}{?tab,other}'
    • Address: /dynamic_url_example/10?tab=1&other=abc
  • All paths and queries values are extracted and included in a RouteData.data instance. E.g.:

    • {'id': '10', 'tab': '1', 'other': 'abc'}
  • This istance is passed as param to the PageBuilder function - static Page route(RouteData data)...

  • A new Page instance is created and included at the Route Stack - you check that easily using the dev tools.

    class DynamicURLPage extends StatefulWidget {
    final int tabIndex;
    const DynamicURLPage({Key? key, required this.tabIndex}) : super(key: key);

    @override
    _DynamicURLPageState createState() => _DynamicURLPageState();
    
    // You don't need to use a static function as Builder, 
    // but it seems to be a good way to organize things   
    static Page route(RouteData data) {
      final tab = data.values['tab'] == 'books' ? 0 : 1;
      return MaterialPage(
        key: const ValueKey('DynamicURLPage'), // Important! Always include a key
        child: DynamicURLPage(tabIndex: tab),
      );
    }
    

    }

4 - Navigating to Pages:

Example Link: All Navigating Examples

4.1 - To navigate to a route with query variables:

  • Template: /dynamic_url_example{?tab,other}

  • Address: /dynamic_url_example?tab=books&other=abc

    APSNavigator.of(context).push(
    path: '/dynamic_url_example',
    params: {'tab': 'books', 'other': 'abc'}, // Add query values in [params]
    );

4.2 - To navigate to a route with path variables:

  • Template: /posts/{post_id}

  • Address: /posts/10

    APSNavigator.of(context).push(
    path: '/post/10', // set path values in [path]
    );

4.3 - You can also include params that aren't used as query variables:

  • Template: /static_url_example

  • Address: /static_url_example

    APSNavigator.of(context).push(
    path: '/static_url_example',
    params: {'tab': 'books'}, // It'll be added to [RouteData.values['tab']]
    );

Details

1. Dynamic URLs Example

Example Link: Dynamic URLs Example

dynamic_url_example

When using dynamic URLs, changing the app's state also changes the browser's URL. To do that:

  • Include queries in the templates. E.g: /dynamic_url_example{?tab}

  • Call updateParams method to update browser's URL:

    final aps = APSNavigator.of(context);
    aps.updateParams(
      params: {'tab': index == 0 ? 'books' : 'authors'},
    );
    
  • The method above will include a new entry on the browser's history.

  • Later, if the user selects such entry, we can recover the previous widget's State using:

    @override
    void didUpdateWidget(DynamicURLPage oldWidget) {
      super.didUpdateWidget(oldWidget);
      final values = APSNavigator.of(context).currentConfig.values;
      tabIndex = (values['tab'] == 'books') ? 0 : 1;
    }
    

What is important to know:

  • Current limitation: Any value used at URL must be saved as string.
  • Don't forget to include a Key on the Page created by the PageBuilder to everything works properly.

2. Static URLs Example

Example Link: Static URLs Example

static_url_example

When using static URLs, changing the app's state doesn't change the browser's URL, but it'll generate a new entry on the history. To do that:

  • Don't include queries on route templates. E.g: /static_url_example

  • As we did with Dynamic's URL, call updateParams method again:

    final aps = APSNavigator.of(context);
    aps.updateParams(
      params: {'tab': index == 0 ? 'books' : 'authors'},
    );
    
  • Then, allow State restoring from browser's history:

    @override
    void didUpdateWidget(DynamicURLPage oldWidget) {
      super.didUpdateWidget(oldWidget);
      final values = APSNavigator.of(context).currentConfig.values;
      tabIndex = (values['tab'] == 'books') ? 0 : 1;
    }
    

What is important to know:

  • Don't forget to include a Key on the Page created by the PageBuilder to everything works properly.

3. Return Data Example

Example Link: Return Data Example

return_data_example

Push a new route and wait the result:

  final selectedOption = await APSNavigator.of(context).push(
     path: '/return_data_example',
  );

Pop returning the data:

  APSNavigator.of(context).pop('Do!');

What is important to know:

  • Data will only be returned once.

  • In case of user navigate your app and back again using the browser's history, the result will be returned at didUpdateWidget method as result, instead of await call.

    @override
    void didUpdateWidget(HomePage oldWidget) {
      super.didUpdateWidget(oldWidget);
      final params = APSNavigator.of(context).currentConfig.values;
      result = params['result'] as String;
      if (result != null) _showSnackBar(result!);
    }
    

4. Multi Push

Example Link: Multi Push Example

multi_push_example

Push a list of the Pages at once:

  APSNavigator.of(context).pushAll(
    // position: (default is at top)
    list: [
      ApsPushParam(path: '/multi_push', params: {'number': 1}),
      ApsPushParam(path: '/multi_push', params: {'number': 2}),
      ApsPushParam(path: '/multi_push', params: {'number': 3}),
      ApsPushParam(path: '/multi_push', params: {'number': 4}),
    ],
  );

In the example above ApsPushParam(path: '/multi_push', params: {'number': 4}), will be the new top.

What is important to know:

  • You don't necessarily have to add at the top; you can use the position param to add the routes at the middle of Route Stack.
  • Don't forget to include a Key on the Page created by the PageBuilder to everything works properly.

5. Multi Remove

Example Link: Multi Remove Example
multi_remove_example

Remove all the Pages you want given a range:

  APSNavigator.of(context).removeRange(start: 2, end: 5);

6. Internal (Nested) Navigators

Example Link: Internal Navigator Example

internal_nav_example-1

class InternalNavigator extends StatefulWidget {
  final String initialRoute;

  const InternalNavigator({Key? key, required this.initialRoute})
      : super(key: key);

  @override
  _InternalNavigatorState createState() => _InternalNavigatorState();
}

class _InternalNavigatorState extends State<InternalNavigator> {
  late APSNavigator childNavigator = APSNavigator.from(
    parentNavigator: navigator,
    initialRoute: widget.initialRoute,
    initialParams: {'number': 1},
    routes: {
      '/tab1': Tab1Page.route,
      '/tab2': Tab2Page.route,
    },
  );

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    childNavigator.interceptBackButton(context);
  }

  @override
  Widget build(BuildContext context) {
    return Router(
      routerDelegate: childNavigator,
      backButtonDispatcher: childNavigator.backButtonDispatcher,
    );
  }
}

What is important to know:

  • Current limitation: Browser's URL won't update based on internal navigator state

GitHub

https://github.com/guilherme-v/aps_navigator