routex 1.0.9

  • Readme
  • Changelog
  • Example
  • Installing
  • 66

Routex #

pub pub

Identify your logic with URI, apply any number of composable asynchronous or synchronous handlers using intuitive syntax and return anything as a result, all that with powerful and flexible error handling and testing.

Since 1.0.5 version: #

  • Route with regex:
router
  .routeWithRegex(r"^\/images\/(?<name>[a-zA-Z0-9]+).(jpg|png|gif|bmp)$")
  .handler((context) => context.response().end(context.getParam("name")));

Route with regex tests

  • Mount sub-router:
//support old CountriesController
  var subRouter = Router.router();
  var countriesController = CountriesController();
  countriesController.bindRouter(subRouter);
  router.mountSubRouter("/v1", subRouter);

Mount sub-router tests

Routex in action #

Example app has built in parallel with framework, and it has over 20+ examples, designed to show framework capabilities, composition, error handling, and RoutexNavigator - Routex consumer ready to use in your app.

Each snippet in this document comes from example app:

Try to notice relation between screens and code in example.dart file:

void main() => runApp(AppWidget());

void bindRouter(Router router) {

  router
    .route("/*")
    .handler(AppComponentHandler()); //basic app dependencies, available on app level

  router
    .routeWithRegex(r"^(\/v1)?\/app/*") //each route that starts with /app/ or /v1/app requires authenthicated user, and user component for di
    .handler(AuthHandler(redirectTo: "/public/login")) //redirects to /public/login if user isn't presented.
    .handler(syncHandlerBetweenTwoAsyncs)
    .handler(UserComponentHandler()); //creates user component

  router.route("/public/login").handler((context) =>
    context.response().end((_) => LoginScreen(context.get<AppComponent>(AppComponent.key).setUser)));

  router
    .route("/app/main")
  //.handler((context) => throw "Exceptions are propagated to failureHandlers or left to global error handlers.")
    .handler(mainScreen)
    .failureHandler((context) =>
    context.response().end((_) =>
      Text("if some exception happens you can" +
        " continue contex with any number of failure handlers, or you can show error screen " +
        "or simply omit failureHandlers and propagate error to global error handlers.")));

  List<Controller> controllers = [TestController(), SearchCountriesController(), ExamplesController()];

  controllers.forEach((controller) => controller.bindRouter(router));
  
  //support old CountriesController, mountSubRouter
    var subRouter = Router.router();
    var countriesController = CountriesController();
    countriesController.bindRouter(subRouter);
    router.mountSubRouter("/v1", subRouter);

  //Controller is just convinient way to group related routes and handlers, it doesn't have any other purpose
  //    abstract class Controller {
  //    void bindRouter(Router router);
  //    }
}

void syncHandlerBetweenTwoAsyncs(RoutingContext context) =>
  context.put("sync_handler_between_two_asyncs", "Hello ${context.get<User>(User.key).name} :)").next();

//equivalent of .handler((context) => context.response().end((_) => MainScreen()))
WidgetBuilder mainScreen(RoutingContext context) => (_) => MainScreen();

class AppWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //To support hot reload in development, use RoutexNavigator.newInstance() to ensure new instance on each reload
    //otherwise just use RoutexNavigator.shared and instance will be automatically created.
    bindRouter(RoutexNavigator.newInstance().router);

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: AppTheme.instance,
      home: RoutexNavigator.shared.get("/app/main")(context),
    );
  }
}

Main app content:

  • Test tab shows how to start another screen for result using handler and ability to log out.
  • Countries tab gives you idea how to integrate Routex in your app with other code.
  • Examples tab shows usage of router directly with streams without RoutexNavigator (ExamplesController => ExamplesSource => ExamplesScreen)

About #

In handlers execute any code that can potentially change your main flow of execution or to get any dependencies and then create cleaner models to work with inherited widgets, streams pattern as you used to.

Idea for handling events in that way comes from Vert.x.
Routex in combination with dart and hot reload is worth of programmers attention.
It's amazing experience if you are interested in flutter and mobile development.

The main part of Dart VM is an event loop. Independent pieces of code can register callback functions as event handlers for certain types of events. Vert.x also achieves that in Java environment with Vert.x event loop.
See Routex in action, 10 concurrent request were executed to populate ListView widget in examples tab of example application(even if one request is irresponsible and in total two are left to you to fix it for practice).
Typically one request is one screen but in Routex you can request for anything, not just screens-WidgetBuilder.

Handlers #

Handler can be anything of this:

Handler<RoutingContext>
Future<void> Function(RoutingContext)
void Function(RoutingContext)
//last two are special cases, they are used as input for response.end(fn)
WidgetBuilder Function(RoutingContext context)
Widget Function(RoutingContext context)

Handler example:

class AuthHandler implements Handler<RoutingContext> {
  final String redirectTo;

  AuthHandler({this.redirectTo});

  @override
  Future<void> handle(RoutingContext context) async {
    //AppComponentHandler is handled before and here we use: context.get<AppComponent>(AppComponent.key)
    var appComponent = context.get<AppComponent>(AppComponent.key);
    Objects.requireNonNull(appComponent);

    User user = await appComponent.loadUser();

    if (user != null) {
      context.put(User.key, user);
      context.next();
    } else {
      if (redirectTo != null) {
        context.reroute(redirectTo);
      } else {
        context.fail(401);
      }
    }
  }
}

Handlers will be executed in exact order each time with every request, in this example AuthHandler consumes AppComponent provided by previously executed handler. It also puts user into context, and that user info is used in another handler.
If something is wrong in app flow, it is possible to reroute request to another route, explicitly fail context, or if some exception happens, work will continue in failure handlers for that route if any, or in global error handlers defined for router if any, or if nothing of that resolves error, context will fail, and that error will be assigned to future associated with request. That means that code executed inside handlers acts as a safe zone and cant be unhandled exceptions thrown. More
But is it possible to ever get unhandled exception while you are working with Routex?
If you use async keywoard in void function, your handler will be treated as synchronous and any thrown exceptions will be unhandled, it is possible in dart to await void functions but Routex doesn't use that approach for handlers case, avoid combination of void and async keyword, instead use Future

RoutexNavigator #

RoutexNavigator is wrapper for interacting with Router, it is place for defining any custom FutureBuilder, error screen, global error handlers, it always use WidgetBuilder as request type, see example app.
It main method is get -> WidgetBuilder, and once that we have WidgetBuilder, we just use Material's Navigator for actions. It's highly customizable, as seen in snippet below:

  Future<T> push<T extends Object>(String path, BuildContext context, [Map<String, dynamic> params]) =>
    Navigator.push(context, MaterialPageRoute(builder: asWidgetBuilder(path, params)));

  Future<T> pushReplacement<T extends Object>(String path, BuildContext context, [Map<String, dynamic> params]) =>
    Navigator.pushReplacement(context, MaterialPageRoute(builder: asWidgetBuilder(path, params)));

  Future<T> replaceRoot<T extends Object>(String path, BuildContext context, [Map<String, dynamic> params]) =>
    Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: asWidgetBuilder(path, params)), (_) => false,);

  Future<WidgetBuilder> asWidgetBuilderFuture(String path, [Map<String, dynamic> params]) =>
    _router.handle(BaseRequest<WidgetBuilder>(path, params: params)).asFuture();

  WidgetBuilder asWidgetBuilder(String path, [Map<String, dynamic> params]) {
     var future = asWidgetBuilderFuture(path, params);
     return (ctx) => _futureObserver(future);
   }
   
  WidgetBuilder get(String path, [Map<String, dynamic> params]) =>
    asWidgetBuilder(path, params);

  Observable<WidgetBuilder> asWidgetBuilderStream(String path, [Map<String, dynamic> params]) =>
    Observable.fromFuture(asWidgetBuilderFuture(path, params));
  
  router.errorHandler(404, (context) => context.response().end(_errorScreen(ResponseStatusException(404))));
  router.errorHandler(500, (context) => context.response().end(_errorScreen(ResponseStatusException(500, context.failure))));
  
  Widget _futureObserver(Future<WidgetBuilder> wb) =>
      futureBuilder != null ?
      futureBuilder(wb) :
      FutureObserver<WidgetBuilder>(
        future: wb,
        onSuccess: (ctx, widget) => widget(ctx),
      );
  
  WidgetBuilder Function(ResponseStatusException) _errorScreen;
  
  Widget Function(Future<WidgetBuilder> wb) futureBuilder;

1.0.9 #

  • Default placeholders update

1.0.8 #

  • Dependencies update

1.0.7 #

  • Dependencies and docs update

1.0.6 #

  • Navigator customization options
RoutexNavigator navigator = RoutexNavigator.newInstance(navigator: CustomNavigator());

class CustomNavigator extends RoutexNavigator {
  
  @override
  Future<T> push<T extends Object>(String path, BuildContext context,
          [Map<String, dynamic> params]) =>
      Navigator.push(
        context,
        _pageRoute(builder: asWidgetBuilder(path, params)),
      );
  
  PageRoute<T> _pageRoute<T>({@required WidgetBuilder builder}) =>
      Platform.isIOS
          ? CupertinoPageRoute(builder: builder)
          : MaterialPageRoute(builder: builder);
  
}

1.0.5 #

  • Route with regex:
router
  .routeWithRegex(r"^\/images\/(?<name>[a-zA-Z0-9]+).(jpg|png|gif|bmp)$")
  .handler((context) => context.response().end(context.getParam("name")));
  • Mount sub-router:
//support old CountriesController
  var subRouter = Router.router();
  var countriesController = CountriesController();
  countriesController.bindRouter(subRouter);
  router.mountSubRouter("/v1", subRouter);

1.0.4 #

Important! #

RoutexNavigator behaviour change.

Before this version behaviour was if Flutter framework calls returned WidgetBuilder function n times to redraw screen, handlers will be executed same number of times. This is not case anymore.
New behaviour is to call handlers once per RoutexNavigator methods call, so 1 call to RoutexNavigator (get, push, etc. methods) is 1 call to all handlers, and Flutter can recall provided WidgetBuilder functions n times, but handlers will be executed only once. This can be very important and useful, for example, if we push screen once we know that we create objects only once, so Flutter calls of WidgetBuilder function will not affect our state - objects created in handlers.
Be aware of differences between get and push methods, push is used explicitly with some event, and get is used mainly in widget tree by framework.
With RoutexNavigator.get call inside Widget tree only guarantee for some objects to be only once initialized is initState method inside State<T> class, and for RoutexNavigator.push calls objects created inside handlers will also be initialized only once.

1.0.3 #

  • Description update

1.0.2 #

  • Description update

1.0.1 #

  • First version

example/lib/main.dart

import 'package:example/base/view.dart';
import 'package:example/controllers/inject_components_controller.dart';
import 'package:flutter/material.dart';
import 'package:routex/routex.dart';

import 'apps/navigator_customization/custom_navigator.dart';
import 'controllers/animation/animation_with_hooks_controller.dart';
import 'controllers/countries_controller.dart';
import 'controllers/examples_controller.dart';
import 'controllers/posts_controller.dart';
import 'controllers/search_countries_controller.dart';
import 'controllers/test_controller.dart';
import 'handlers/app_component_handler.dart';
import 'handlers/auth_handler.dart';
import 'handlers/user_component_handler.dart';
import 'model/user.dart';
import 'theme/theme.dart';
import 'widgets/login_screen.dart';
import 'widgets/main_screen.dart';

//void main() => runApp(NavigatorCustomizationApp());
void main() => runApp(AppWidget());
//void main() => runApp(TipsApp());

void bindRouter(Router router) {
  //RoutexNavigator by default applies handlers for 404 and 500 error codes. (That behaviour is controlled by applyDefaultHandlers parameter)
  //Whatever we throws or error,exceptions that happened will have at the end error code 500.
  //But numbers have special meaning. If we throw(number) or do context.fail(number) global handlers like this can be applied.
  //Codes 500 and 404 tells you that it is not desired flow, and you should always resolve errors in failureHandlers, look error handling documentation and example app on how it is done.
  router.errorHandler(
      401,
      (context) => context.response().end(
          (_) => RoutexNavigatorErrorScreen(ResponseStatusException(401))));

  router.route("/*").handler(
      AppComponentHandler()); //basic app dependencies, available on app level

  router
      .routeWithRegex(
          r"^(\/v1)?\/app/*") //each route that starts with /app/ or /v1/app requires authenthicated user, and user component for di
      .handler(AuthHandler(
          redirectTo:
              "/public/login")) //redirects to /public/login if user isn't presented.
      .handler(syncHandlerBetweenTwoAsyncs)
      .handler(UserComponentHandler()); //creates user component

  router
      .route("/public/login")
      .handler((context) => context.response().end((_) => LoginScreen()));

  router
      .route("/app/main")
//  .handler((context) => throw "Exceptions are propagated to failureHandlers or left to global error handlers.")
      .handler(mainScreen)
      .failureHandler((context) => context.response().end((_) => Text(
          "if some exception happens you can" +
              " continue contex with any number of failure handlers, or you can show error screen " +
              "or simply omit failureHandlers and propagate error to global error handlers.")));

  List<Controller> controllers = [
    InjectComponentsController(),
    TestController(),
    SearchCountriesController(),
    ExamplesController(),
    PostsController(),
    AnimationWithHooksController(),
  ];

  controllers.forEach((controller) => controller.bindRouter(router));

  //support old CountriesController, mountSubRouter
  var subRouter = Router.router();
  var countriesController = CountriesController();
  countriesController.bindRouter(subRouter);
  router.mountSubRouter("/v1", subRouter);

  //Controller is just convinient way to group related routes and handlers, it doesn't have any other purpose
  //    abstract class Controller {
  //    void bindRouter(Router router);
  //    }
}

void syncHandlerBetweenTwoAsyncs(RoutingContext context) => context
    .put("sync_handler_between_two_asyncs",
        "Hello ${context.get<User>(User.key).name} :)")
    .next();

//equivalent of .handler((context) => context.response().end((_) => MainScreen()))
WidgetBuilder mainScreen(RoutingContext context) => (_) => MainScreen();

// ignore: must_be_immutable
class AppWidget extends BaseView {
  @override
  Widget buildWidget(BuildContext context) {
    //To support hot reload in development, use RoutexNavigator.newInstance() to ensure new instance on each reload
    //otherwise just use RoutexNavigator.shared and instInjectComponentsControllerance will be automatically created.
    bindRouter(
        RoutexNavigator.newInstance(navigator: CustomNavigator()).router);

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: AppTheme.instance,
      home: managedView(
        "/app/main/",
//        "/app/animation/with-tabs",
//        "/app/animation/",
        //comment to start with login screen
        {"user": User("Flutter user")},
      )(context),
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  routex: ^1.0.9

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:routex/routex.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
36
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
90
Overall:
Weighted score of the above. [more]
66
Learn more about scoring.

We analyzed this package on Jun 4, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.2
  • pana: 0.13.8-dev
  • Flutter: 1.17.1

Health suggestions

Fix lib/src/widgets/routex_navigator_error_screen.dart. (-0.50 points)

Analysis of lib/src/widgets/routex_navigator_error_screen.dart reported 1 hint:

line 19 col 52: 'title' is deprecated and shouldn't be used. This is the term used in the 2014 version of material design. The modern term is headline6. This feature was deprecated after v1.13.8..

Maintenance issues and suggestions

Support latest dependencies. (-10 points)

The version constraint in pubspec.yaml does not support the latest published versions for 1 dependency (rxdart).

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.6.0 <3.0.0
flutter 0.0.0
rxdart ^0.23.1 0.23.1 0.24.1
Transitive dependencies
collection 1.14.12
meta 1.1.8
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test