Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] Add Routing Support #41

Open
felangel opened this issue Mar 25, 2021 · 18 comments · May be fixed by #99
Open

[Proposal] Add Routing Support #41

felangel opened this issue Mar 25, 2021 · 18 comments · May be fixed by #99
Assignees
Labels
enhancement New feature or request feedback wanted Looking for feedback from the community

Comments

@felangel
Copy link
Owner

felangel commented Mar 25, 2021

[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>(
      ...
    )
  }
}
@felangel felangel added enhancement New feature or request help wanted Extra attention is needed labels Mar 25, 2021
@felangel felangel self-assigned this Mar 25, 2021
@felangel felangel mentioned this issue Mar 25, 2021
@felangel felangel pinned this issue Mar 25, 2021
@felangel felangel added feedback wanted Looking for feedback from the community and removed help wanted Extra attention is needed labels Mar 25, 2021
@orestesgaolin
Copy link
Contributor

orestesgaolin commented Mar 25, 2021

Regarding nested routes. Would such thing as below be possible? It feels more inline with routes being the representation of state, where you don't have to explicitly push any new routes onto the stack and all the routes in the Navigator have corresponding pages.

class AppRouting extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlowBuilder<AuthState>(
        state: context.watch<AuthBloc>().state,
        onGeneratePages: (AuthState state, List<Page> pages) {
          switch (state) {
            case AuthState.uninitialized:
              return [Splash.page()];
            case AuthState.unauthenticated:
            case AuthState.signInInProgress:
              return [
                Login.page(),
                if (state == AuthState.signInInProgress) SignInRouting.page(),
              ];
            case AuthState.authenticated:
              return [Home.page()];
          }
        });
  }
}

enum AuthState {
  uninitialized,
  unauthenticated,
  signInInProgress,
  authenticated
}

class SignInRouting extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlowBuilder<SignInState>(
        state: context.watch<SignInBloc>().state,
        onGeneratePages: (SignInState state, List<Page> pages) {
          switch (state) {
            case SignInState.step1:
              return [SignInStep1.page()];
            case SignInState.step2:
              return [SignInStep2.page()];
            case SignInState.verify:
              return [SignInVerify.page()];
          }
        });
  }
}

enum SignInState {
  step1,
  step2,
  verify,
}

Of course when using enums like AuthState it's hard to represent more complex states where it can have 2 dimensions (e.g. authentication state and sign in process started), but with class it could be more readable.

class AppRouting extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlowBuilder<AppState>(
        state: context.watch<AppState>().state,
        onGeneratePages: (AppState state, List<Page> pages) {
          switch (state.authState) {
            case AuthState.uninitialized:
              return [Splash.page()];
            case AuthState.unauthenticated:
              return [
                Login.page(),
                if (state.signState == SignState.signIn) 
                  SignInRouting.page(),
                if (state.signState == SignState.signUp) 
                  SignUpRouting.page(),
              ];
            case AuthState.authenticated:
              return [Home.page()];
          }
        });
  }
}

class AppState {
  AppState(this.authState, this.signState);
  final AuthState authState;
  final SignState signState;
}

enum SignState {
  signIn,
  signUp,
}

enum AuthState { uninitialized, unauthenticated, authenticated }

class SignInRouting extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlowBuilder<SignInState>(
        state: context.watch<SignInBloc>().state,
        onGeneratePages: (SignInState state, List<Page> pages) {
          switch (state) {
            case SignInState.step1:
              return [SignInStep1.page()];
            case SignInState.step2:
              return [SignInStep2.page()];
            case SignInState.verify:
              return [SignInVerify.page()];
          }
        });
  }
}

enum SignInState {
  step1,
  step2,
  verify,
}

@orestesgaolin
Copy link
Contributor

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

What would be structure of FlowLocation? Currently RouteInformation contains location property being a string. I find having Uri a bit more convenient, but it also imposes some constraints like validity of schema or path. Would that .path property be a string as well?

@Gene-Dana
Copy link

Gene-Dana commented Mar 25, 2021

For everyones benefit, here are the scenarios that this proposal would support: https://github.com/flutter/uxr/blob/master/nav2-usability/storyboards/%5BPublic%5D%20Flutter%20Navigator%20Scenarios%20Storyboards%20v2.pdf

I believe the onLocationChange and the FlowLocation really make sense for the deep linking path and query parameters and now I can clearly see how we can declaratively do this.

With the FlowLocation, we really just add a named parameter and that's pretty simple.

As always, it's not so much about what the API looks like as to how you use the API ! This means those little examples could go a long way and I look forward to helping apply the examples to these scenarios.

The real feedback will come when more people use it and we uncover desire paths. I personally cannot imagine any more scenarios and I have full faith in the communities ability to use this in ways we never imagined, for better or for worse !

@Gene-Dana
Copy link

Gene-Dana commented Mar 25, 2021

In your examples, you have this function which I think really helps being up at the top of the file

List<Page> onGenerateProfilePages(Profile profile, List<Page> pages) {
  return [
    MaterialPage<void>(child: ProfileNameForm()),
    if (profile.name != null) MaterialPage<void>(child: ProfileAgeForm()),
    if (profile.age != null) MaterialPage<void>(child: ProfileWeightForm()),
  ];
}

Would you suggest similar for onLocationUpdate?

@theweiweiway
Copy link

Sorry to derail a bit - is the intention of this to be a full drop in replacement for all routing within an app, or an enhancement for better flow support that should be used in conjunction with an existing routing solution?

@herman-the-worm
Copy link

For those who need the web working today you can try auto_route with authguard + authentication bloc

There are some limitations but it works somehow

@Gene-Dana
Copy link

Sorry to derail a bit - is the intention of this to be a full drop in replacement for all routing within an app, or an enhancement for better flow support that should be used in conjunction with an existing routing solution?

This can do both. Whether you would like to make it for a full replacement, or piecewise, that's the flexibility it offers

For those who need the web working today you can try auto_route with authguard + authentication bloc

There are some limitations but it works somehow

I think you will appreciate this ! https://github.com/felangel/bloc/tree/master/examples/flutter_firebase_login

@adamsmaka
Copy link
Contributor

@felangel any ETA of this proposal?

@felangel
Copy link
Owner Author

felangel commented Jun 8, 2021

@felangel any ETA of this proposal?

Sorry for the delay! I'm planning to pick this up later this week or this weekend 👍

@magicleon94
Copy link

magicleon94 commented Jul 23, 2021

Are there any updates about this? This would be very helpful for some deep link cases me and my team are working on!

Thank you all for your efforts!

@felangel
Copy link
Owner Author

felangel commented Nov 3, 2021

For anyone interested, there's a WIP branch for this proposal at https://github.com/felangel/flow_builder/tree/feat/url-routing-and-deep-linking 🎉

@PRAJINPRAKASH
Copy link

@felangel How sync the browser url?

@filly82
Copy link

filly82 commented Jan 25, 2022

For anyone interested, there's a WIP branch for this proposal at https://github.com/felangel/flow_builder/tree/feat/url-routing-and-deep-linking 🎉

I am interested, but it is not working as expected

Here is my code

FlowBuilder<NavigationStack>(
              state: context.watch<NavigationCubit>().state,
              onGeneratePages: (NavigationStack state, List<Page> pages) {
                print(state.all);
                return state.all.map((e){
                  return MaterialPage<dynamic>(child: MyScreen.getByRouteLocation(e.location).widget);
                }).toList();
              },
              onDidPop: (dynamic result) {
                print('hallo=$result');
                context.watch<NavigationCubit>().popRoute();
              },
              onLocationChanged: (Uri uri, NavigationStack stack) {
                print('uri=$uri');
                print('stack=$stack');
                return stack;
              },
            )

onDidPop is never called, onLocationChanged only on initial routing when starting the app.
everything else works fine, I can see that the state is correctly updated and routing/navigation works also fine

@filly82
Copy link

filly82 commented Jan 25, 2022

the problem seems to be that FlowBuilder expects the state to be a single route, which in my case would be a NavigationRoute, but my state is a NavigationStack with a List<NavigationRoute>

_history has always 1 item in my case, the NavigationStack, that's why the onDidPop never is called
_pages however has the correct amount of items (routes)

child: Navigator(
          key: _navigatorKey,
          pages: _pages,
          observers: [_FlowNavigatorObserver(), ...widget.observers],
          onPopPage: (route, dynamic result) {

            if (_history.length > 1) { // <-- problem lies here
              _history.removeLast();
              _didPop = true;
              widget.onDidPop?.call(result);
              _controller.update((_) => _history.last);
            }
            if (_pages.length > 1) _pages.removeLast();
            final onLocationChanged = widget.onLocationChanged;
            final pageLocation = _pages.last.name;
            if (onLocationChanged != null && pageLocation != null) {
              _SystemNavigationObserver._updateLocation(pageLocation);
              _controller.update(
                (state) => onLocationChanged(Uri.parse(pageLocation), state),
              );
            }
            setState(() {});
            return route.didPop(result);
          },
        )

is this a Bug or do I need to rethink my state for Navigation?

UPDATE:
I was just playing around and made sure that _history and _pages have always the same amount of items, and everything worked as I was expecting it with the popping

@MdeBruin93
Copy link

Any update on this?

@SAGARSURI
Copy link

Looking forward for this feature to be added.

@iamnabink
Copy link

So, it's mainly suitable for onboarding or other limited page flows, or has anyone successfully used it as a comprehensive routing solution ? or possibly alongside packages like go_router or auto_route?

@herman-the-worm
Copy link

herman-the-worm commented Oct 30, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request feedback wanted Looking for feedback from the community
Projects
None yet
Development

Successfully merging a pull request may close this issue.