Skip to content

[Proposal] Add Routing Support #41

@felangel

Description

@felangel

[Proposal] Add Routing Support

Currently FlowBuilder does not have custom routing support for deep linking, dynamic linking, custom paths, query parameters, browser url synchronization, etc (#20).

This proposal outlines a potential FlowBuilder API which can accomodate for the above use-cases by leveraging the Navigator 2.0 router APIs. Ideally, the proposed enhancements should be backward compatible with the existing API.

Routing API

FlowBuilder<FlowState>(
  // The state of the flow.
  state: FlowState(),
  // `VoidCallback` invoked whenever the route location changes.
  // Responsible for reacting to changes in the location.
  //
  // * Will be invoked immediately when the app is launched
  // to determine what the initial state of the flow should be.
  // * Will be invoked when the current route changes via `pushNamed`, `pushReplacementNamed`, etc.
  onLocationChanged: (BuildContext context, FlowLocation location) {
    // Equivalent to `window.location.pathname`.
    final String path = location.path;

    // Map of query parameters which are analagous to `window.location.search`.
    final Map<String, String> params = location.params;

    // Determine the flow state based on the current location.
    final FlowState state = _determineFlowState(path, params);

    /// Update the flow state.
    context.flow<FlowState>().update((_) => state);
  },
  // Called whenever the flow state changes. Will remaing unchanged.
  // Responsible for determining the correct navigation stack
  // based on the current flow state.
  onGeneratePages: (FlowState state, List<Page> currentPages) {
    final List<Page> pages = _determinePages(state, currentPages);
    return pages;
  }
)

Execution Flow

  1. FlowBuilder is initialized with a state
  2. onLocationChanged is invoked when a location change occurs.
  3. Flow state can be updated based on the location change.
  4. onGeneratePages is triggered when the flow state changes & updates the nav stack.

The developer can optionally react to changes in FlowLocation (abstraction on top of RouteInformation) and trigger updates in flow state.

Named Routes

Named routes can be achieved by defining a FlowPage which extends Page.

const profilePath = 'profile';

...

FlowBuilder<Profile>(
  state: Profile(),
  onLocationChanged: (BuildContext context, FlowLocation location) {
    if (location.path != profilePath) return;
    // Alternatively we can potentially use `fromJson` with `package:json_serializable`.
    // `final profile = Profile.fromJson(location.params);`
    final profile = Profile(
      name: location.params['name'] as String?,
      age: int.tryParse(location.params['age'] as String?)
    );
    context.flow<Profile>().update((_) => profile);
  },
  onGeneratePages: (Profile profile, List<Page> pages) {
    return [
      FlowPage<void>(
        child: ProfileNameForm(),
        location: FlowLocation(path: profilePath),
      ),
      if (profile.name != null)
        FlowPage<void>(
          child: ProfileAgeForm(),
          location: FlowLocation(path: profilePath, params: {'name': profile.name})
        ),
    ]
  }
)

The above code will result in the following state to route mapping:

  • Profile() (default): /profile
  • Profile(name: 'Felix'): /profile?name=Felix
  • Profile(name: 'Felix', age: 26): /profile?name=Felix&age=26

Navigation

Navigation will largely remain unchanged. Using context.flow or a FlowController, developers can update the flow state or complete the flow. The main difference would be updates to the flow state can potentially be accompanied by location changes if the associated pages are of type FlowPage with a custom location. When a named route is pushed via Navigator.of(context).pushNamed, all available FlowBuilder instances will be notified via onLocationChanged.

Nested Routes

FlowBuilders can be nested to support nested routing. For example:

enum AuthState { uninitialized, unauthenticated, authenticated }

class AuthBloc extends Bloc<AuthEvent, AuthState> {...}

FlowBuilder<AuthState>(
  state: context.watch<AuthBloc>().state,
  onGeneratePages: (AuthState state, List<Page> pages) {
    switch (state) {
      case AuthState.uninitialized:
        return [Splash.page()];
      case AuthState.unauthenticated:
        return [Login.page()];
      case AuthState.authenticated:
        return [Home.page()];
    }
  }
)

We can push a nested flow from within Login to initiate a Sign Up flow.

class Login extends StatelessWidget {

  ...

  @override
  Widget build(BuildContext context) {
    ...
    ElevatedButton(
      onPressed: () => Navigator.of(context).push(SignUp.page());
    )
  }
}
class SignUp extends StatelessWidget {

  ...

  @override
  Widget build(BuildContext context) {
    return FlowBuilder<SignUpState>(
      ...
    )
  }
}

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestfeedback wantedLooking for feedback from the community

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions