Nolence
Nolence

Reputation: 2254

Declarative auth routing with Firebase

Rather than pushing the user around with Navigator.push when they sign in or out, I've been using a stream to listen for sign in and sign out events.

StreamProvider<FirebaseUser>.value(
  value: FirebaseAuth.instance.onAuthStateChanged,
)

It works great for the home route as it handles logging in users immediately if they're still authed.

Consumer<FirebaseUser>(
  builder: (_, user, __) {
    final isLoggedIn = user != null;

    return MaterialApp(
      home: isLoggedIn ? HomePage() : AuthPage(),
      // ...
    );
  },
);

However, that's just for the home route. For example, if the user then navigates to a settings page where they click a button to sign out, there's no programmatic logging out and kicking to the auth screen again. I either have to say Navigator.of(context).pushNamedAndRemoveUntil('/auth', (_) => false) or get an error about user being null.

This makes sense. I'm just looking for possibly another way that when they do get logged out I don't have to do any stack management myself.

I got close by adding the builder property to the MaterialApp

builder: (_, widget) {
  return isLoggedIn ? widget : AuthPage();
},

This successfully moved me to the auth page after I was unauthenticated but as it turns out, widget is actually the Navigator. And that means when I went back to AuthPage I couldn't call anything that relied on a parent Navigator.

Upvotes: 2

Views: 821

Answers (2)

Nolence
Nolence

Reputation: 2254

I found a way to accomplish this (LoVe's great answer is still completely valid) in case anyone else steps on this issue:

You'll need to take advantage of nested navigators. The Root will be the inner navigator and the outer navigator is created by MaterialApp:

return MaterialApp(
  home: isLoggedIn ? Root() : AuthPage(),
  routes: {
    Root.routeName: (_) => Root(),
    AuthPage.routeName: (_) => AuthPage(),
  },
);

Your Root will hold the navigation for an authed user

class Root extends StatefulWidget {
  static const String routeName = '/root';

  @override
  _RootState createState() => _RootState();
}

class _RootState extends State<Root> {
  final _appNavigatorKey = GlobalKey<NavigatorState>();
  
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        final canPop = _appNavigatorKey.currentState.canPop();

        if (canPop) {
          await _appNavigatorKey.currentState.maybePop();
        }

        return !canPop;
      },
      child: Navigator(
        initialRoute: HomePage.routeName,
        onGenerateRoute: (RouteSettings routeSettings) {
          return MaterialPageRoute(builder: (_) {
            switch (routeSettings.name) {
              case HomePage.routeName:
                return HomePage();
              case AboutPage.routeName:
                return AboutPage();
              case TermsOfUsePage.routeName:
                return TermsOfUsePage();
              case SettingsPage.routeName:
                return SettingsPage();
              case EditorPage.routeName:
                return EditorPage();
              default:
                throw 'Unknown route ${routeSettings.name}';
            }
          });
        },
      ),
    );
  }
}

Now you can unauthenticate (FirebaseAuth.instance.signout()) inside of the settings page (or any other page) and immediately get kicked out to the auth page without calling a Navigator method.

Upvotes: 1

HII
HII

Reputation: 4109

What about this,you wrap all your screens that depend on this stream with this widget which hides from you the logic of listening to the stream and updating accordingly(you should provide the stream as you did in your question):

class AuthDependentWidget extends StatelessWidget {
  final Widget childWidget;

  const AuthDependentWidget({Key key, @required this.childWidget})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: FirebaseAuth.instance.onAuthStateChanged,
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        if (snapshot.hasData) {//you handle other cases...
          if (snapshot.currentUser() != null) return childWidget();
        } else {
          return AuthScreen();
        }
      },
    );
  }
}

And then you can use it when pushing from other pages as follows:

Navigator.of(context).pushReplacement(MaterialPageRoute(
        builder: (ctx) => AuthDependentWidget(
              childWidget: SettingsScreen(),//or any other screen that should listen to the stream
            )));

Upvotes: 1

Related Questions