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

best way to handle navigation from a reducer? #5

Closed
MichaelRFairhurst opened this issue Jan 28, 2018 · 8 comments
Closed

best way to handle navigation from a reducer? #5

MichaelRFairhurst opened this issue Jan 28, 2018 · 8 comments

Comments

@MichaelRFairhurst
Copy link

MichaelRFairhurst commented Jan 28, 2018

I have a Ticker streaming events to my store, each of which does some tiny math. At a certain threshold, the app should navigate.

It is mildly complicated to find this event, so I could detect this case and do this within the Ticker -- though even then, that's kinda gross as I need a context and I don't actually know how safe it is to just navigate with the last context passed into build() (since the Ticker is inherently firing outside the build method and such).

class MyState extends State {
  final Store s;
  final Context context;
  Ticker t = new Ticker((_) {
    s.dispatch(...);
    if (s.wordPosition + ... > s.???) Navigator.of(context).pushBuilder(...);
  });
  build(c) {
    context = c;
    ...
  }
}

I then figured it'd be better if I have a singleton Stream that can exchange this data. I'm not entirely sure how safe it is in the context of hot reload and such, if I should be pushing out from one Store into a Stream which affects the app elsewhere...but, I figure, that's a small enough concern I may as well do this to unblock me.

reduceEvent(State s, Event e) {
  final newPosition = ...;
  if (newPosition > s.???) {
    s.onDoneSink.add(null);
  }
  return s.copyWith(....);
}

I realized that I have the same problem of getting the context -- I could use a StreamBuilder, but that would always, on each build, get me the most recent navigation pushed to my stream. Not what I want. And if I don't use StreamBuilder, I'm not a builder, and so I don't have a context to go off of.

  class MyApp {
    Stream s;
    build() => new StreamBuilder(s, (context, _, currentValue) {
      if (currentValue) // the last value is irrelevant to whether we should navigate right now
        Navigator.of(context).pushBuilder(...);
      return new Container();
   };

I'm not convinced this shouldn't be a middleware, but they don't seem well documented. I think navigation is often done in a middleware with react/redux, but don't actually follow the code examples well, and they lean heavily on the navigation APIs itself which are different on the web.

I'm starting to realize, anyways, that Navigator is probably basically just altering the state of the MaterialApp by probably looking up some kind of InheritedNavigationWidget or something

class Navigator extends InheritedWidget {
  Stack<String> routes;
}
class MaterialApp {
  build(context) =>
    Navigator.of(context).buildCurrentRoute();
}

So this makes me think I shouldn't be navigating at all, but that my reducer should set page = Pages.Histogram in my state, and then my MyApp component can do all routing with that. Downside here is that I have to reimplement things like push/pop/replace.

build() =>
  new MaterialApp(
    child: new StoreConnector(
      store: s,
      converter: getRoute,
      builder: (_, route) {
        switch (route) {
          ...
        }
      }
    )
  );

Is this a solved problem, am I on the right track that I should just reimplement navigation in redux because navigation is just a form of state management anyway built through less testable techniques?

@xqwzts
Copy link
Contributor

xqwzts commented Jan 29, 2018

FYI you can access your app's Navigator without a context by setting the navigatorKey property of your MaterialApp:

  /// A key to use when building the [Navigator].
  ///
  /// If a [navigatorKey] is specified, the [Navigator] can be directly
  /// manipulated without first obtaining it from a [BuildContext] via
  /// [Navigator.of]: from the [navigatorKey], use the [GlobalKey.currentState]
  /// getter.
  ///
  /// If this is changed, a new [Navigator] will be created, losing all the
  /// application state in the process; in that case, the [navigatorObservers]
  /// must also be changed, since the previous observers will be attached to the
  /// previous navigator.
  final GlobalKey<NavigatorState> navigatorKey;

Create the key:

final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();

Pass it to MaterialApp:

new MaterialApp(
      title: 'MyApp',
      onGenerateRoute: generateRoute,
      navigatorKey: key,
    );

Push routes:

navigatorKey.currentState.pushNamed('/someRoute');

@xqwzts
Copy link
Contributor

xqwzts commented Jan 29, 2018

I find it preferable to use this to navigate in a side-effect action/middleware and leave navigation state handling to flutter.

@MichaelRFairhurst
Copy link
Author

ah, awesome! Totally didn't look deeper at the navigation API one bit. HUGE thanks, that at the very least unblocks me!

Is there any documentation on how to write a middleware? That's another thing I didn't see. Middleware does indeed seems like the right place for this.

@brianegan
Copy link
Owner

Thanks for writing in and to xqwzts for the good answer! On vacation for a few more days, but you can find some docs on Middleware here: https://github.com/johnpryan/redux.dart/blob/master/doc/async.md

@MichaelRFairhurst
Copy link
Author

I guess there's no reason to leave this open. Thanks for the help, everyone!

@sjmcdowall
Copy link

If anyone is still reading this -- I am trying this but it's not working all that well -- when I get an outside event .. and want to go to a particular screen .. it goes there -- but loses ALL context such as theme / backbitten / app bar stuff / etc. It's a plain screen and no way to navigate ..

This is an existing screen that is navigable via App Bar buttons, etc. So ... how can this work?

Cheers

@temirfe
Copy link

temirfe commented Mar 21, 2019

navigatorKey.currentState.pushNamed('/someRoute');

it would be nice if you also showed how did you get reference to navigatorKey in navigatorKey.currentState.pushNamed('/someRoute');
because in my case editor is complaining that navigatorKey is undefined

@brianegan
Copy link
Owner

@temirfe You have two options:

  1. Make the navigatorKey a global variable (probably wouldn't do this myself)
  2. Create a Middleware that accepts the navigatorKey (my preferred mechanism)

In code using a MiddlewareClass:

class MyMiddleware extends MiddlewareClass<AppState> {
  final GlobalKey<NavigatorState> navigatorKey;

  MyMiddleware(this.navigatorKey);
 
  @override
  void call(Store<AppState> store, dynamic action, NextDispatcher next) {
    if (action is SomeAction) {
      navigatorKey.currentState.pushNamed('/someRoute');
    }
  }
}

To provide this to your Store:

void main() {
  final navigatorKey = new GlobalKey<NavigatorState>();
  final store = Store(myReducer, middleware: [MyMiddleware(navigatorKey)]);

  runApp(StoreProvider(store: store, child: MaterialApp(navigatorKey: navigatorKey)));
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants