DEV Community

Alexandre
Alexandre

Posted on

[Flutter] Firebase authentication : Dynamic routing by AuthStateChanges πŸ”₯

Hi there πŸ‘‹

Today, dynamic routing with Firebase by AuthStateChanges.

What we want ? We want routing on LoginPage or HomePage automatically when user state changed.

If user is connected and suddenly token expired (or is disconnected), we want redirect to LoginPage.

Initialize app

First of all, we starting by initialized Firebase app.
(In this example, no catch, but think to catch errors).

import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // TODO: Handle error for Firebase.initializeApp
  await Firebase.initializeApp();

  runApp(MyApp());
}
Enter fullscreen mode Exit fullscreen mode

Define our pages and routes

Create two pages HomePage and LoginPage. And add it to onGenerateRoute of the MyApp widget.

UnknownPage for all others pages. 😎

1. HomePage

This page will disconnect our user.
Think to add a simple loader for example while requesting.

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

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

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: ListView(
      padding: EdgeInsets.all(32),
      children: [
        ElevatedButton(
          onPressed: () {
            FirebaseAuth.instance.signOut();
          },
          child: Text('Sign out'),
        )
      ],
    ));
  }
}
Enter fullscreen mode Exit fullscreen mode

2. LoginPage

This page will connect our user. We'll use signInAnonymously for the example. But use what you need ! πŸ‘Œ

class LoginPage extends StatefulWidget {
  const LoginPage({Key? key}) : super(key: key);

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

class _LoginPageState extends State<LoginPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: ListView(
      padding: EdgeInsets.all(32),
      children: [
        ElevatedButton(
          onPressed: () {
            FirebaseAuth.instance.signInAnonymously();
          },
          child: Text('Sign In'),
        )
      ],
    ));
  }
}
Enter fullscreen mode Exit fullscreen mode

3. UnknownPage

Just a basic red Container

class UnknownPage extends StatefulWidget {
  const UnknownPage({Key? key}) : super(key: key);

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

class _UnknownPageState extends State<UnknownPage> {
  @override
  Widget build(BuildContext context) {
    return Container(color: Colors.red);
  }
}
Enter fullscreen mode Exit fullscreen mode

4. MyApp

Add a stateful widget MyApp with onGenerateRoute.

Handle with a switch the settings.name route.

And return MaterialPageRoute with all right pages.

Add also initialRoute.

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'HelloWorld',
      // ----- Check if user exists => First page app
      initialRoute:
          FirebaseAuth.instance.currentUser == null ? 'login' : 'home',
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case 'home':
            return MaterialPageRoute(
              settings: settings,
              builder: (_) => HomePage(),
            );
          case 'login':
            return MaterialPageRoute(
              settings: settings,
              builder: (_) => LoginPage(),
            );
          default:
            return MaterialPageRoute(
              settings: settings,
              builder: (_) => UnknownPage(),
            );
        }
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Time to be reactive

1. GlobalKey

final _navigatorKey = GlobalKey<NavigatorState>();
Enter fullscreen mode Exit fullscreen mode

This is the key, πŸ”‘

All we want, all we need, is this one.

With this GlobalKey, we have the control to navigate in our app.

class _MyAppState extends State<MyApp> {
final _navigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'HelloWorld',
      navigatorKey: _navigatorKey, // <===== HERE
      onGenerateRoute: (settings) {
      // ...
Enter fullscreen mode Exit fullscreen mode

2. InitState

Add the initState function to listen FirebaseAuth changes.

  late StreamSubscription<User?> _sub;

  @override
  void initState() {
    super.initState();
    _sub = FirebaseAuth.instance.authStateChanges().listen((event) {
      _navigatorKey.currentState!.pushReplacementNamed(
        event != null ? 'home' : 'login',
      );
    });
  }
Enter fullscreen mode Exit fullscreen mode

3. Dispose

⛔️ Don't forget to dispose your last subscription.

  @override
  void dispose() {
    _sub.cancel();
    super.dispose();
  }
Enter fullscreen mode Exit fullscreen mode

Done βœ…

Now all code available here :

import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late StreamSubscription<User?> _sub;
  final _navigatorKey = new GlobalKey<NavigatorState>();

  @override
  void initState() {
    super.initState();

    _sub = FirebaseAuth.instance.userChanges().listen((event) {
      _navigatorKey.currentState!.pushReplacementNamed(
        event != null ? 'home' : 'login',
      );
    });
  }

  @override
  void dispose() {
    _sub.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'HelloWorld',
      navigatorKey: _navigatorKey,
      initialRoute:
          FirebaseAuth.instance.currentUser == null ? 'login' : 'home',
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case 'home':
            return MaterialPageRoute(
              settings: settings,
              builder: (_) => HomePage(),
            );
          case 'login':
            return MaterialPageRoute(
              settings: settings,
              builder: (_) => LoginPage(),
            );
          default:
            return MaterialPageRoute(
              settings: settings,
              builder: (_) => UnknownPage(),
            );
        }
      },
    );
  }
}

class UnknownPage extends StatefulWidget {
  const UnknownPage({Key? key}) : super(key: key);

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

class _UnknownPageState extends State<UnknownPage> {
  @override
  Widget build(BuildContext context) {
    return Container(color: Colors.red);
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

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

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: ListView(
      padding: EdgeInsets.all(32),
      children: [
        ElevatedButton(
          onPressed: () {
            FirebaseAuth.instance.signOut();
          },
          child: Text('Sign Out'),
        )
      ],
    ));
  }
}

class LoginPage extends StatefulWidget {
  const LoginPage({Key? key}) : super(key: key);

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

class _LoginPageState extends State<LoginPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: ListView(
      padding: EdgeInsets.all(32),
      children: [
        ElevatedButton(
          onPressed: () {
            FirebaseAuth.instance.signInAnonymously();
          },
          child: Text('Sign In'),
        )
      ],
    ));
  }
}
Enter fullscreen mode Exit fullscreen mode

One more thing

There is many level of details for auth changes.

  /// Notifies about changes to the user's sign-in state (such as sign-in or
  /// sign-out).
  Stream<User?> authStateChanges() =>
      _pipeStreamChanges(_delegate.authStateChanges());

  /// Notifies about changes to the user's sign-in state (such as sign-in or
  /// sign-out) and also token refresh events.
  Stream<User?> idTokenChanges() =>
      _pipeStreamChanges(_delegate.idTokenChanges());

  /// This is a superset of both [authStateChanges] and [idTokenChanges]. It
  /// provides events on all user changes, such as when credentials are linked,
  /// unlinked and when updates to the user profile are made. The purpose of
  /// this Stream is for listening to realtime updates to the user state
  /// (signed-in, signed-out, different user & token refresh) without
  /// manually having to call [reload] and then rehydrating changes to your
  /// application.
Stream<User?> userChanges() => _pipeStreamChanges(_delegate.userChanges());
Enter fullscreen mode Exit fullscreen mode

See you soon 🐼

Discussion (0)