Skip to content

Conversation

@Hixie
Copy link
Contributor

@Hixie Hixie commented Jun 21, 2017

The main purpose of this PR is to make it so that when you set the
initial route and it's a hierarchical route (e.g. /a/b/c), it
implies multiple pushes, one for each step of the route (so in that
case, /, /a, /a/b, and /a/b/c, in that order). If any of those
routes don't exist, it falls back to '/'.

As part of doing that, I:

  • Changed the default for MaterialApp.initialRoute to honor the
    actual initial route.

  • Added a MaterialApp.onUnknownRoute for handling bad routes.

  • Added a feature to flutter_driver that allows the host test script
    and the device test app to communicate.

  • Added a test to make sure flutter drive --route works.
    (Hopefully that will also prove flutter run --route works, though
    this isn't testing the flutter tool's side of that. My main
    concern is over whether the engine side works.)

  • Fixed flutter drive to output the right target file name.

  • Changed how the stocks app represents its data, so that we can
    show a page for a stock before we know if it exists.

  • Made it possible to show a stock page that doesn't exist. It shows
    a progress indicator if we're loading the data, or else shows a
    message saying it doesn't exist.

  • Changed the pathing structure of routes in stocks to work more
    sanely.

  • Made search in the stocks app actually work (before it only worked
    if we happened to accidentally trigger a rebuild). Added a test.

  • Replaced some custom code in the stocks app with a BackButton.

  • Added a "color" feature to BackButton to support the stocks use case.

  • Spaced out the ErrorWidget text a bit more.

  • Added RouteSettings.copyWith, which I ended up not using.

  • Improved the error messages around routing.

While I was in some files I made a few formatting fixes, fixed some
code health issues, and also removed flaky: true from some devicelab
tests that have been stable for a while. Also added some documentation
here and there.

@Hixie
Copy link
Contributor Author

Hixie commented Jun 21, 2017

Sorry this is such a giant patch.

cc @yjbanov for review of the flutter driver and devicelab parts
cc @HansMuller for review of everything else

@Hixie
Copy link
Contributor Author

Hixie commented Jun 21, 2017

Fixes #10813
Fixes #618

Copy link
Contributor

@yjbanov yjbanov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM w/ comments.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a utility function in utils.dart (already imported) called section that you can use to print headings like this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say 2 minutes is optimistic but ok playing by ear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed timeout, i'll let cocoon deal with it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is 2mm away from being a custom request handler that takes a String and returns a String. 2 things would close the 2mm gap:

  1. rename this method and DataHandler to something that implies "you can do anything in the DataHandler and return a result"
  2. have DataHandler return Future<String> instead of String

I feel like we should do it now so we don't have to break API or introduce an almost identical API later because users want to do more than request data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed DataHandler to return a future string.

Any ideas what requestData, RequestData, DataHandler, and RequestDataResult should be called, if not those? I couldn't come up with anything more generic...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideas: RequestWithData, CustomRequest, SimpleRequest

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to avoid words like Simple in APIs because if the API is simple, that should be obvious, and if it's not, the name will be a constant source of anger.

"customRequest()" doesn't really convey anything different than "requestData()" but is less verb-like, which is suboptimal for a method.

"requestWithData()" vs "requestData()" seems like a wash except the latter is shorter, so...

I dunno. I've left it as requestData() for now. I don't feel strongly about this though, feel free to change it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this returned Future<String> it could be used for more use-cases (such as being able to wait for data to become available). You shouldn't lose anything in generality given that _requestData below and the network separation by its nature are already asynchronous.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: += is not incorrect, but ++ is much more idiomatic in Dart code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with ++ is that in some situations, the postfix operator is less efficient than += 1. Since loops are often performance critical, people get all worried about this and end up writing ++moves. And the prefix operator is super ugly and horrible. So I'm on a mission to entirely kill the ++ and -- operators. :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this? The docs on addOption don't seem to explain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I filed a bug on the docs being suboptimal.

It makes the help say that the syntax is --keep-app-running=<url>.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in flutter drive you still need the -t

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, this is not the case. I ran it with "flutter drive --route '/smuggle-it' lib/route.dart" and it worked.

@collinjackson
Copy link
Contributor

collinjackson commented Jun 21, 2017

Perhaps we should skip null intermediate route names rather than bailing out entirely.

We could make the routing behavior configurable via a delegate so that if developers don't like the default behavior for deep links, they could easily replace it with a different deep link handling algorithm. (This could be done in a separate CL, but should probably be addressed before make a lot of noise about this feature.)

@lukef
Copy link
Contributor

lukef commented Jun 22, 2017

I agree with @collinjackson. We may have a route like /users/:id and /users/create but no /users route so we wouldn't want the app to try to push/restore a /users route out of context or if it doesn't exist.

As a side, we don't structure our routes hierarchically like this. I'm not sure if that is the intention of routing in flutter or if this is just a convenience?

Also, it might be handy for navigator to have a push that allows pushing multiple routes / widgets at the same time (something like setViewControllers on the iOS NavigationController). That way you can decide where the pop will go to.

@Hixie
Copy link
Contributor Author

Hixie commented Jun 22, 2017

@lukef Do you want the / route to be pushed when you start the app at /users/create? If so, would it work for you to name the route /users:create instead? Or is it important to you that it be named /users/create? (Just trying to understand the space here.)

@lukef
Copy link
Contributor

lukef commented Jun 22, 2017

Route names are generally of no consequence. It was more just an understanding that this is how you guys considered the routing to work. We didn't assume that if we have a /users/create route that it necessitated having a /users handler in between. If that's how Navigator works (or will) then its good to know.

re: root. I feel like there are times when you will want / to be restored every time and times when you don't want it. There are times where you might want a deep link to show an article and have to do the equivalent of FLAG_ACTIVITY_CLEAR_TOP or opening a new VC on iOS. To be clear, i'm not sure if this is in the scope of flutter.

The example I gave was around a news app displaying an article. If I get a push notification i might want to open the app and display the article. If the user hits close, then i want to just exit the app because the expectation is that the stack would only have the article screen/widget and nothing else. Other times I could see it would be desirable to have the option for pop to take me back to root. I would say it isn't something I'd expect flutter to make a choice for me on.

@Hixie
Copy link
Contributor Author

Hixie commented Jun 22, 2017

@lukef The current patch only applies this model if the strings starts with a /. Would that be sufficient control for you? (Basically, if you want to have the root route pushed, then the user-create route, you'd set the initialRoute to /users:create; if you wanted to just open an article, you'd set it to something like article:name-of-article-foo-bar, etc.)

Based on @collinjackson's idea, my long-term plan is to make this pluggable, so next to the routes table you could provide a route handler delegate that implements the behavior, and then you could just do whatever you want with the initialRoute (and presumably any other routes that get pushed later).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that explains what you're up to here would be helpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added lots of comments here.

@lukef
Copy link
Contributor

lukef commented Jun 22, 2017

That would work. It might be handy for me to show you our set up soon also because we're running a somewhat different setup (https://github.com/goposse/fluro) and that creates interesting side effects.

My personal wish would be a mechanism where we can just receive the incoming deep link info and deal with it ourselves manually if we wanted :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we close the http client first?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Close the http client first? I assume that there's some finalization protocol that closes the connection at GC, but it seems at least tidier to just explicitly close it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@Hixie
Copy link
Contributor Author

Hixie commented Jun 22, 2017

My personal wish would be a mechanism where we can just receive the incoming deep link info and deal with it ourselves manually if we wanted :)

You can do that with this patch -- if you set initialRoute on the MaterialApp, it'll ignore the incoming initial route entirely and just do what you want.

@lukef
Copy link
Contributor

lukef commented Jun 22, 2017

That's what we are doing now. I set initial route to null and then I use onGenerateRoute to determine where to go. We're just seeing side effects from that approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a comment or an issue that explains what's needed to complete this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably use the NumberFormat class here, just to demo how to format prices.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to leave this for now and we can revisit it when we do i18n.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty slick!

Copy link
Contributor

@HansMuller HansMuller Jun 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another way to format this, to make the if then else structure a little clearer:

secondChild: stock != null 
  ? new _StockSymbolView(
     stock: stock,
     arrow: new Hero(
      tag: stock,
      child: new StockArrow(percentChange: stock.percentChange),
     ),
   ),
 : new Padding(
    padding: const EdgeInsets.all(20.0),
    child: new Center(child: new Text('$symbol not found')),
   ),
),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe explain why you're waiting a minute at a time? I would have thought that a no-params pumpAndSettle() call would have been OK.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you want to merge this sentence into the previous paragraph.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow, i don't know what i was doing there. removed that second sentence.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really want to describe this in Android-specific terms ("intent")?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just an example. Right now there's no automatic way to do this in iOS.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on => or

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note sure why we refer to this route as a "page"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because routes are pages on the Web and old habits die hard.

Changed the two "page"s to "route"s.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be slightly easier to read if this occurrence of this was [home]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  /// If [home] is specified, then [routes] must not include an entry for `/`,
  /// as [home] takes its place.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "loaded" you mean pushed. You could even refer to [Navigator.push] once in this explanation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.
Added to see-also.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this => this process

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need to use a generic term, like "deep link", here and link to a TBD explanation of how the idea maps to something concrete on iOS and Android.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a mention of "deep link".

We could put the docs you mention in Navigator.initialRoute. I'm not sure what they should say though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will only see => the onGenerateRoute callback will only be applied to

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, fixed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need the braces?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will only see => the onGenerateRoute callback will only be applied to

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as before; loaded => pushed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but with => with

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skipped per our discussion

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember a character named HOOLI appearing in Hamilton

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skipped per our discussion

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the value be routeSeparator instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed routeSeparator instead

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to have a constant instead of a convention, then our examples and framework probably need a survey, to see where '/' is assumed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could put the initial values on the RHS instead of using .add()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@HansMuller
Copy link
Contributor

LGTM modulo some doc word smithing and various nits.

I think we're going to need provide a comprehensive explanation (somewhere) about about how Flutter deep linking relates to intents on Android and TBD on iOS.

@HansMuller HansMuller closed this Jun 22, 2017
@HansMuller HansMuller reopened this Jun 22, 2017
The main purpose of this PR is to make it so that when you set the
initial route and it's a hierarchical route (e.g. `/a/b/c`), it
implies multiple pushes, one for each step of the route (so in that
case, `/`, `/a`, `/a/b`, and `/a/b/c`, in that order). If any of those
routes don't exist, it falls back to '/'.

As part of doing that, I:

 * Changed the default for MaterialApp.initialRoute to honor the
   actual initial route.

 * Added a MaterialApp.onUnknownRoute for handling bad routes.

 * Added a feature to flutter_driver that allows the host test script
   and the device test app to communicate.

 * Added a test to make sure `flutter drive --route` works.
   (Hopefully that will also prove `flutter run --route` works, though
   this isn't testing the `flutter` tool's side of that. My main
   concern is over whether the engine side works.)

 * Fixed `flutter drive` to output the right target file name.

 * Changed how the stocks app represents its data, so that we can
   show a page for a stock before we know if it exists.

 * Made it possible to show a stock page that doesn't exist. It shows
   a progress indicator if we're loading the data, or else shows a
   message saying it doesn't exist.

 * Changed the pathing structure of routes in stocks to work more
   sanely.

 * Made search in the stocks app actually work (before it only worked
   if we happened to accidentally trigger a rebuild). Added a test.

 * Replaced some custom code in the stocks app with a BackButton.

 * Added a "color" feature to BackButton to support the stocks use case.

 * Spaced out the ErrorWidget text a bit more.

 * Added `RouteSettings.copyWith`, which I ended up not using.

 * Improved the error messages around routing.

While I was in some files I made a few formatting fixes, fixed some
code health issues, and also removed `flaky: true` from some devicelab
tests that have been stable for a while. Also added some documentation
here and there.
@Hixie
Copy link
Contributor Author

Hixie commented Jun 23, 2017

Updated per comments. Will land on green unless I hear otherwise.

I'm going to do the factoring out as a separate step later once we have more experience with this. Filed #10943.

@lukef I'm eager to hear if this causes you any troubles once it's landed.

@Hixie Hixie merged commit 9adb4a7 into flutter:master Jun 23, 2017
@Hixie Hixie deleted the deep-linking branch June 23, 2017 21:58
gspencergoog pushed a commit to gspencergoog/flutter that referenced this pull request Jul 1, 2017
…#10894)

The main purpose of this PR is to make it so that when you set the
initial route and it's a hierarchical route (e.g. `/a/b/c`), it
implies multiple pushes, one for each step of the route (so in that
case, `/`, `/a`, `/a/b`, and `/a/b/c`, in that order). If any of those
routes don't exist, it falls back to '/'.

As part of doing that, I:

 * Changed the default for MaterialApp.initialRoute to honor the
   actual initial route.

 * Added a MaterialApp.onUnknownRoute for handling bad routes.

 * Added a feature to flutter_driver that allows the host test script
   and the device test app to communicate.

 * Added a test to make sure `flutter drive --route` works.
   (Hopefully that will also prove `flutter run --route` works, though
   this isn't testing the `flutter` tool's side of that. My main
   concern is over whether the engine side works.)

 * Fixed `flutter drive` to output the right target file name.

 * Changed how the stocks app represents its data, so that we can
   show a page for a stock before we know if it exists.

 * Made it possible to show a stock page that doesn't exist. It shows
   a progress indicator if we're loading the data, or else shows a
   message saying it doesn't exist.

 * Changed the pathing structure of routes in stocks to work more
   sanely.

 * Made search in the stocks app actually work (before it only worked
   if we happened to accidentally trigger a rebuild). Added a test.

 * Replaced some custom code in the stocks app with a BackButton.

 * Added a "color" feature to BackButton to support the stocks use case.

 * Spaced out the ErrorWidget text a bit more.

 * Added `RouteSettings.copyWith`, which I ended up not using.

 * Improved the error messages around routing.

While I was in some files I made a few formatting fixes, fixed some
code health issues, and also removed `flaky: true` from some devicelab
tests that have been stable for a while. Also added some documentation
here and there.
@ralph-bergmann
Copy link

ralph-bergmann commented Sep 4, 2019

@Hixie I found this merge request and I'm wondering why the deep linking works how it works.

If I have, for example, this initial route: /a/b I have to have this generateRoute switch:

static Route<dynamic> generateRoute(RouteSettings settings) {
  print('generateRoute for ${settings.name}');
  switch (settings.name) {
    case '/':
      return MaterialPageRoute(builder: (_) => HomeView());
    case '/a':
      return MaterialPageRoute(builder: (_) => AView());
    case '/a/b':
      return MaterialPageRoute(builder: (_) => BView());
    default:
      return MaterialPageRoute(
        builder: (_) => Scaffold(
          body: Center(
            child: Text('No route defined for ${settings.name}'),
          ),
        ),
      );
  }
}

Why do you want to generate a route for /, '/a' and /a/b instead of /, '/a' and /b?

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 4, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants