Firebase Login with Flutter using onAuthStateChanged

Viewed 43278

Outside of Flutter, when I implement firebase authentication I always use the onAuthStateChanged listener provided by firebase to determine if the user is logged in or not and respond accordingly.

I am trying to do something similar using flutter, but I can find a way to access onAuthStateChanged of Firebase. I am using the firebase_auth, and google_signin Flutter plugins. I am working of example code that is included with the firebase_auth Flutter plugin. Below is the sample code. I can login successfully with google sign in, but the example is too simple, because I want to have an observer/listener to detect the user's signed in/out state.

Is there a way to detect via observer/listener using the firebase_auth/google_signin flutter plugins to determine the status of a user?

Ultimately I want the app to determine if the user is logged in (yes/no). If not then show a login screen, if yes then show my main app page.

import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';

final FirebaseAuth _auth = FirebaseAuth.instance;
final GoogleSignIn _googleSignIn = new GoogleSignIn();

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Firebase Auth Demo',
      home: new MyHomePage(title: 'Firebase Auth Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Future<String> _message = new Future<String>.value('');

  Future<String> _testSignInAnonymously() async {
    final FirebaseUser user = await _auth.signInAnonymously();
    assert(user != null);
    assert(user == _auth.currentUser);
    assert(user.isAnonymous);
    assert(!user.isEmailVerified);
    assert(await user.getToken() != null);
    if (Platform.isIOS) {
      // Anonymous auth doesn't show up as a provider on iOS
      assert(user.providerData.isEmpty);
    } else if (Platform.isAndroid) {
      // Anonymous auth does show up as a provider on Android
      assert(user.providerData.length == 1);
      assert(user.providerData[0].providerId == 'firebase');
      assert(user.providerData[0].uid != null);
      assert(user.providerData[0].displayName == null);
      assert(user.providerData[0].photoUrl == null);
      assert(user.providerData[0].email == null);
    }
    return 'signInAnonymously succeeded: $user';
  }

  Future<String> _testSignInWithGoogle() async {
    final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
    final GoogleSignInAuthentication googleAuth =
        await googleUser.authentication;
    final FirebaseUser user = await _auth.signInWithGoogle(
      accessToken: googleAuth.accessToken,
      idToken: googleAuth.idToken,
    );
    assert(user.email != null);
    assert(user.displayName != null);
    assert(!user.isAnonymous);
    assert(await user.getToken() != null);
    return 'signInWithGoogle succeeded: $user';
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          new MaterialButton(
              child: const Text('Test signInAnonymously'),
              onPressed: () {
                setState(() {
                  _message = _testSignInAnonymously();
                });
              }),
          new MaterialButton(
              child: const Text('Test signInWithGoogle'),
              onPressed: () {
                setState(() {
                  _message = _testSignInWithGoogle();
                });
              }),
          new FutureBuilder<String>(
              future: _message,
              builder: (_, AsyncSnapshot<String> snapshot) {
                return new Text(snapshot.data ?? '',
                    style: const TextStyle(
                        color: const Color.fromARGB(255, 0, 155, 0)));
              }),
        ],
      ),
    );
  }
}

Here are links to the flutter packages in question: https://github.com/flutter/plugins/tree/master/packages/firebase_auth https://github.com/flutter/plugins/tree/master/packages/google_sign_in

7 Answers

I know this question is pretty old, but here is the answer if anybody is still looking for it.

Firebase returns a Stream of FirebaseUser with it's onAuthStateChanged function. There are many ways to listen to the user's authentication state change. This is how I do it:

Solution 1

I return a StreamBuilder to my App's home page, and the StreamBuilder returns specific pages based on the auth status of the user.

@override
Widget build(BuildContext context) {
  return MaterialApp(
      title: 'Your App Name',
      home: _getLandingPage()
  );
}

Widget _getLandingPage() {
  return StreamBuilder<FirebaseUser>(
    stream: FirebaseAuth.instance.onAuthStateChanged,
    builder: (BuildContext context, snapshot) {
      if (snapshot.hasData) {
        if (snapshot.data.providerData.length == 1) { // logged in using email and password
          return snapshot.data.isEmailVerified
              ? MainPage()
              : VerifyEmailPage(user: snapshot.data);
        } else { // logged in using other providers
          return MainPage();
        }
      } else {
        return LoginPage();
      }
    },
  );
}

Solution 2

You can create a listener in your app's initState() function as well. Make sure the firebase app has been initialized before registering the listener.

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

  FirebaseAuth.instance.authStateChanges().listen((firebaseUser) {
    // do whatever you want based on the firebaseUser state
  });
}

Solution 3 (Update May 2021)

A simple approach with null-safety without using the provider package:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

/// State is persistent and not rebuilt, therefore [Future] is only created once.
/// If [StatelessWidget] is used, in the event where [App] is rebuilt, that
/// would re-initialize FlutterFire and makes our app re-enter the
/// loading state, which is undesired.
class _AppState extends State<App> {
  final Future<FirebaseApp> _initFirebaseSdk = Firebase.initializeApp();
  final _navigatorKey = new GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      navigatorKey: _navigatorKey,
      theme: theme(),
      home: FutureBuilder(
          future: _initFirebaseSdk,
          builder: (_, snapshot) {
            if (snapshot.hasError) return ErrorScreen();

            if (snapshot.connectionState == ConnectionState.done) {
              // Assign listener after the SDK is initialized successfully
              FirebaseAuth.instance.authStateChanges().listen((User? user) {
                if (user == null)
                  _navigatorKey.currentState!
                      .pushReplacementNamed(LoginScreen.routeName);
                else
                  _navigatorKey.currentState!
                      .pushReplacementNamed(HomeScreen.routeName);
              });
            }

            return LoadingScreen();
          }),
      routes: routes,
    );
  }
}

This approach guarantees that you only use Firebase authentication FirebaseAuth.instance.authStateChanges().listen() after the SDK completes initialization. The auth change listener will be first invoked on app launch and then automatically called again after logout and login.

.pushReplacementNamed() will move to a new screen without back (no back icon on the app bar)

Null safe code (without 3rd party packages)

Screenshot:

enter image description here


To check if the user is signed in from anywhere in the app, use

bool signedIn = Auth.instance.isSignedIn;

To sign in, use

await Auth.instance.signIn(email: 'email', password: 'password');

To sign out, use

await Auth.instance.signOut();

Full Code:


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

  runApp(
    MaterialApp(
      home: StreamBuilder<User?>(
        stream: Auth.instance.authStateChange(),
        builder: (_, snapshot) {
          final isSignedIn = snapshot.data != null;
          return isSignedIn ? HomePage() : LoginPage();
        },
      ),
    ),
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('HomePage')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Auth.instance.signOut(),
          child: Text('Sign out'),
        ),
      ),
    );
  }
}

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('LoginPage')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Auth.instance.signIn(email: 'test@test.com', password: 'test1234'),
          child: Text('Sign in'),
        ),
      ),
    );
  }
}

class Auth {
  static final instance = Auth._();
  Auth._();

  final FirebaseAuth _auth = FirebaseAuth.instance;
  bool get isSignedIn => _auth.currentUser != null;

  Stream<User?> authStateChange() => _auth.authStateChanges();

  Future<void> signIn({required String email, required String password}) => _auth.signInWithEmailAndPassword(email: email, password: password);

  Future<void> signOut() => _auth.signOut();
}

Same code using provider package:

Check this answer:

You can create a stream as a getter for the onAuthStateChanged inside an AuthService class. To help you manage the state, you can use the Provider package. The AuthService class will extend the ChangeNotifier class.

class AuthService extends ChangeNotifier {

    final FirebaseAuth _auth = FirebaseAuth.instance;
    final GoogleSignIn _googleSignIn = new GoogleSignIn();

    // create a getter stream
    Stream<FirebaseUser> get onAuthStateChanged => _auth.onAuthStateChanged;

    //Sign in async functions here ..

}

Wrap your MaterialApp with ChangeNotifierProvider and return an instance of the AuthService class in create method like so:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => AuthService(),
      child: new MaterialApp(
      title: 'Firebase Auth Demo',
      home: Landing(),
      ),
    );
  }
}

Now create landing page as a stateless widget. Use Provider.of(context) and a stream builder to listen to the auth changes and render the login page or home page as appropriate.

class Landing extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    AuthService auth = Provider.of<AuthService>(context);
    return StreamBuilder<FirebaseUser>(
      stream: auth.onAuthStateChanged,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.active) {
          FirebaseUser user = snapshot.data;
          if (user == null) {
            return LogIn();
          }
          return Home();
        } else {
          return Scaffold(
            body: Center(
              child: CircularProgressIndicator(),
            ),
          );
        }
      },
    );
  }
}

You can read more about state management with provider from the official flutter documentation. Follow this link: https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple

Null safe code (using provider):

enter image description here

Full Code:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  
  runApp(
    MaterialApp(
      home: ChangeNotifierProvider(
        create: (_) => AuthModel(),
        child: Consumer<AuthModel>(
          builder: (_, model, __) => model.isSignedIn ? HomePage() : LoginPage(),
        ),
      ),
    ),
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('HomePage')),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            final model = context.read<AuthModel>();
            await model.signOut();
          },
          child: Text('Sign out'),
        ),
      ),
    );
  }
}

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('LoginPage')),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            final model = context.read<AuthModel>();
            await model.signIn(email: 'test@test.com', password: 'test1234');
          },
          child: Text('Sign in'),
        ),
      ),
    );
  }
}

class AuthModel extends ChangeNotifier {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  bool get isSignedIn => _auth.currentUser != null;

  Future<void> signIn({required String email, required String password}) async {
    await _auth.signInWithEmailAndPassword(email: email, password: password);
    notifyListeners();
  }

  Future<void> signOut() async {
    await _auth.signOut();
    notifyListeners();
  }
}

since the last update of FlutterFire you need to do it like this

FirebaseAuth.instance.authStateChanges().listen((User user) {
    if (user == null) {
      print('User is currently signed out!');
    } else {
      print('User is signed in!');
    }
  }
);

Firebase Auth enables you to subscribe in realtime to this state via a Stream. Once called, the stream provides an immediate event of the users current authentication state, and then provides subsequent events whenever the authentication state changes.

To subscribe to these changes, call the authStateChanges() method on your FirebaseAuth instance.

The stream returns a User class if the user is signed in, or null if they are not. You can read more about managing your users below.

More information

Related