47

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

Eric Seidel
  • 3,282
  • 1
  • 15
  • 22
Gilberg
  • 2,514
  • 4
  • 31
  • 41

7 Answers7

72

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)

CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
Javid Noutash
  • 2,142
  • 17
  • 19
  • Hi, I know its been a while, but when I first start my app, firebaseUser's value is FirebaseUser(Instance of 'PlatformUser'), even when nobody is signed in. When logging out, the value goes to null, but why isn't it null to begin with? – Larry Jing Jun 11 '20 at 19:27
  • 1
    There is a serious issue with this approach. Since it is using Streambuilder, this stuff will get called everytime you navigate. See issue here https://stackoverflow.com/questions/56835372/flutter-stream-builder-triggered-when-navigator-pop-or-push-is-called – giorgio79 Aug 17 '20 at 07:41
  • 1
    onAuthStateChanged is deprecated, you should go with authStateChanges() – LoopsGod Nov 27 '20 at 19:24
  • 1
    In the 2nd solution, there is one scenario where FirebaseAuth might be used before the Firebase Core SDK finishes its initialization. U need to guarantee the core SDK is initialized first so that u can add a listener to authStateChanges – Son Nguyen May 29 '21 at 09:17
  • I tried the solution 1 and 2 does not work, I have tried. – Shajeel Afzal Jun 08 '21 at 12:46
  • Greate solution. Solution 3 worked for me. Thanks:-) – Soorya Jul 10 '21 at 18:14
12

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:

CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
7

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

humanshado
  • 190
  • 1
  • 4
6

The Firebase for Flutter Codelab has a much more in-depth example using Google sign in and Firebase auth.

After the final step you end up with this _ensureLoggedIn function that is used to check whether the user is signed in and if not, initiate a sign in flow.

Future<Null> _ensureLoggedIn() async {
  GoogleSignInAccount user = googleSignIn.currentUser;
  if (user == null)
    user = await googleSignIn.signInSilently();
  if (user == null) {
    user = await googleSignIn.signIn();
    analytics.logLogin();
  }
  if (auth.currentUser == null) {
    GoogleSignInAuthentication credentials =
    await googleSignIn.currentUser.authentication;
    await auth.signInWithGoogle(
      idToken: credentials.idToken,
      accessToken: credentials.accessToken,
    );
  }
}

You could modify this to check these things when your app starts up and conditionally show different views to pre-auth and post-auth users with something like:

final auth = FirebaseAuth.instance;

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        title: 'MyApp',
        home: (_checkLogin() == true ? new PostAuthScaffold() : new PreAuthScaffold())
    );
  }
}

bool _checkLogin() {
  GoogleSignInAccount user = googleSignIn.currentUser;
  return !(user == null && auth.currentUser == null);
}
FrederickCook
  • 3,018
  • 4
  • 24
  • 26
  • 4
    Hi @FrederickCook - thanks for your answer, I was hoping it would solve my issue with this also but when I debug your _checkLogin() function I see that auth.currentUser will always return a Future even on a device where I have never logged in. So _checkLogin() will always return true and in your case it will probably always show the PostAuthScaffold() widget. Or are you experiencing it differently? – Bjørn Børresen Sep 12 '17 at 07:15
  • 2
    Well the Firebase for Flutter codelab mentions absolutely nothing about authentication at any place. The link you have shared is for a name app using Firestore, nothing to do with Authentication. Github link you have shared does contain some lines of auth code but that isn't helpful at all when it comes to how to manage the logged in state. – gegobyte May 20 '20 at 17:08
3

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();
  }
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
1

I had the same query I used Shared preference to get the auth state changed information I also have built a project using Shared Prefrences with Firebase and flutter. Iif you wish to know you can read the blog written on the same by me :

Implementing FirebaseAuthStateListener using Shared Prefrences in Flutter.” https://medium.com/@vaibhavminiyar/implementing-firebaseauthstatelistener-using-shared-prefrences-in-flutter-b42e12f81eb2

Vaibhav Miniyar
  • 743
  • 1
  • 7
  • 17
1

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

Andres Diaz
  • 421
  • 1
  • 5
  • 10