12

I have a WelcomeScreen which contains sign up and login and the HomeScreen where I want to redirect after the user logs in. To manage auth data, I have created an auth.dart with static properties and methods so I can access them across all pages with same data.

import 'package:firebase_auth/firebase_auth.dart';

class Auth {

  static final auth = FirebaseAuth.instance;

  static Future<void> logout() async {
    await auth.signOut();
  }

  static Future<void> loginUser(String userEmail, String userPassword) async {
    await auth.signInWithEmailAndPassword(email: userEmail, password: userPassword);
  }

  static Future<FirebaseUser> getCurrentUser() async {
    return await auth.currentUser();
  }
}

In main.dart file, I am using StreamBuilder to change the current screen based on changing auth data. I got this StreamBuilder code from this answer.

home: StreamBuilder<FirebaseUser>(
  stream: Auth.auth.onAuthStateChanged,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return HomeScreen();
    } else {
      return WelcomeScreen();
    }
  },
),

In my login screen, I am using the below code to trigger log in:

Future<void> login() async {
    ...

    try {
      await Auth.loginUser(userEmail, userPassword);
      var user =  await Auth.getCurrentUser();
      print(user.displayName); // This works
    } catch (error) {
      print(error.message);
    }
  }

I don't know whether the static methods I am using are the correct way to handle Firebase auth or not but it seems to work. After logging in, I am able to display the name of the logged in user but the StreamBuilder in main.dart is not reflecting the updated auth data, i.e not changing the page.

Is it because of static methods or something wrong in the implementation of StreamBuilder?

CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
gegobyte
  • 4,945
  • 10
  • 46
  • 76

4 Answers4

18

Null safe code (using provider)

enter image description here


Full code:

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

  runApp(
    ChangeNotifierProvider<AuthModel>(
      create: (_) => AuthModel(),
      child: MaterialApp(
        home: Consumer<AuthModel>(
          builder: (_, auth, __) => auth.isSignedIn ? HomePage() : WelcomePage(),
        ),
      ),
    ),
  );
}

class WelcomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('WelcomePage')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => LoginPage())),
          child: Text('Go to Login Page'),
        ),
      ),
    );
  }
}

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

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

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
  • Now with Provider, shouldn't that automatically notify Consumer that auth has changed and it should rebuild the correct screen automatically rather than calling pop? – gegobyte May 25 '20 at 07:03
  • @gegobyte Well no, because in Navigator tree, `LoginPage` is on the stack, so it can't be popped automatically. You will have to either pop or push a new route clearing all existing ones. In my previous example, I pushed (which was a kinda heck) but in this one pop is absolutely doing things in a correct way. – CopsOnRoad May 25 '20 at 07:04
  • I am using `pushReplacementNamed` to prevent endless elements getting added to stack as there is option to go to signup page, forgot password page in my code. So using pop on login screen is giving me black screen as there is nothing below it in stack. But just to test it out, I used `pushNamed` instead but got an error `Could not find the correct provider above this HomeScreen widget`. In main.dart I have wrapped MaterialApp with ChangeNotifierProvider and passed Auth in create. – gegobyte May 25 '20 at 10:49
  • Well the error I was getting was because I removed some providers which HomeScreen uses. Fixed that and now everything works. So I am now using `pushNamed` to go to login screen so I can pop. Is it possible to make it work while using `pushReplacementNamed`? – gegobyte May 25 '20 at 11:28
  • In your welcome screen, you are using `Navigator.push` to reach login screen. And then after logging in, you are using `Navigator.pop`, what I was using earlier and would like to use is `Navigator.pushReplacement` but using that will not allow me to use `Navigator.pop` in login screen. So if I use `Navigator.pushReplacement` to navigate to login screen, how do I trigger a rebuild of Consumer in `main.dart` as without popping, I am stuck at login screen even after logging in. – gegobyte May 25 '20 at 13:09
  • In that case, you'll have to `push` a route on the `Navigator`, you can use `pushReplacement` to navigate to the `HomeScreen`. – CopsOnRoad May 25 '20 at 13:10
  • Ok so I did a `pushReplacement` to Home screen after doing login but the logout isn't navigating to Welcome Screen automatically. Home screen is direct child of main.dart unlike login screen so it should navigate back but it isn't working. In the debug console, it does say `Notifying auth state listeners about a sign-out event.` but it only notifies and doesn't rebuild. – gegobyte May 25 '20 at 14:47
  • @CopsOnRoad i used your solution to implement my autheitication but i have an issue i cant solve it can you checj this please https://stackoverflow.com/questions/62085806/authentification-firebase-with-provider-flutter – Merym Jun 02 '20 at 13:15
  • 2
    @FatimaAbdallah That shouldn't be a problem, let me get to my machine tomorrow. – CopsOnRoad Jun 02 '20 at 13:18
  • 1
    This helped me a lot! NOTE: Future should now be Future (FirebaseUser is deprecated). – apteryxlabs Oct 05 '20 at 19:52
4

In my opinion the best way to manage firebase authentication in flutter is to use the provider package. Your Auth class is missing one important thing which is the onAuthStateChnaged method. You can create a stream as a getter for the onAuthStateChanged inside an Auth class. The Auth class will extend the ChangeNotifier class. ChangeNotifier class is part of the flutter api.

class Auth extends ChangeNotifier {

    final FirebaseAuth _auth = FirebaseAuth.instance;

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

    //Sign in async functions here ..

}

Wrap your MaterialApp with ChangeNotifierProvider (part of provider package) and return an instance of the Auth class in create method like so:

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

Now create landing page as a stateless widget. Use a Consumer or 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) {
    Auth auth = Provider.of<Auth>(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
3

Screenshot:

enter image description here


I'm not sure how you were doing it, so I added a minimal working code, I didn't make any changes to your Auth class. Although it is a good idea to use Provider but you can get things done with static method also.

Edited code:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MaterialApp(home: MyApp()));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<FirebaseUser>(
      stream: Auth.auth.onAuthStateChanged,
      builder: (context, snapshot) {
        if (snapshot.hasData) return HomeScreen();
         else return WelcomeScreen();
      },
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home Screen')),
      floatingActionButton: FloatingActionButton.extended(
        label: Text('Sign out'),
        onPressed: Auth.logout,
      ),
    );
  }
}

class WelcomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Welcome Screen')),
      body: Center(
        child: RaisedButton(
          onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => LoginPage())),
          child: Text('Go to Login Page'),
        ),
      ),
    );
  }
}

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login Page')),
      body: Center(
        child: RaisedButton(
          onPressed: () async {
            await Auth.loginUser('test@test.com', 'test1234');
            await Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => MyApp()), (_) => false);
          },
          child: Text('Login'),
        ),
      ),
    );
  }
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • Your stream builder code looks same as mine but navigation to home screen after log in isn't working for me. Can you please look into my code to see where I am getting it wrong? – gegobyte May 24 '20 at 11:17
  • Where are you using your `StreamBuilder`, who is its parent? Can you please post minimal reproducible code? – CopsOnRoad May 24 '20 at 11:21
  • Same as yours in Material app's home, I had already posted it in the question. – gegobyte May 24 '20 at 11:37
  • Which action doesn't reflect correct state, when you sign in or when you sign out. – CopsOnRoad May 24 '20 at 11:45
  • Ok so what happens is I clean start my app and is welcomed by the welcome screen, there are 2 buttons on it - login and signup. I click on login and gets redirected to login screen, enter the credentials and click submit but it doesn't redirect to homescreen but if hot restart the app, then it directly takes me to homescreen which means the signin under the hood worked, simply the navigation failed. Logout button is on homescreen which on press redirects me to welcome screen. Navigation on login is not working. So the stream builder in main.dart is working for logout but not for login. – gegobyte May 24 '20 at 12:22
  • @gegobyte Ok got it, so you're widget tree was creating an issue when you were navigating, I have fixed it and tested it in the same manner you described your problem in previous comment, it's working. Let me know if you need further assistance. – CopsOnRoad May 24 '20 at 12:46
  • So in your updated code, you are explicitly navigating to homescreen after doing login. I am only calling the login user function and then leaving the rest to StreamBuilder because once I call auth on login, Stream Builder which is using `onAuthStateChanged` would be notified that auth status has changed and it should do the navigation rather than me doing it. Isn't this correct? I am not using Navigation after doing log out also, I am just calling the logout function and the stream builder in main.dart does the navigation. – gegobyte May 24 '20 at 12:53
  • 1
    See, your `HomeScreen` is a direct child of `StreamBuilder`, so when you log out from there, you don't need any kind of navigation to show the `WelcomeScreen`, however, the screen from where you're logging in (`LoginPage` here) isn't the direct child of `StreamBuilder`, because you navigated to `LoginPage` using a `Navigator`, so you again need a `Navigator` to exit out of it. If you don't like this kind of behaviour, you can use `ChangeNotifierProvider` which would call all its descendants when you change something (here the status of Authentication) – CopsOnRoad May 24 '20 at 13:00
  • Ok I understand. So using Provider is after all a better method. Can you please post some provider code for this? – gegobyte May 24 '20 at 13:44
  • @gegobyte Yes I can add that but I am out of office now. Will get back to it tomorrow. – CopsOnRoad May 24 '20 at 14:48
1

I made a video(https://youtu.be/iqy7xareuAI) discussing this bounty and taking you through the steps of implementing the app that you want. All it needs is a simple StreamBuilder and a FutureBuilder.

More complex tools like provider and singleton pattern(what you are trying to achieve with static classes) can be applied for more complex applications, but not needed here.

  1. We treat WelcomeScreen as the screen that decides between LoginSignupScreen and HomeScreen
  2. We use StreamBuilder for WelcomeScreen
  3. We use FutureBuilder for HomeScreen.

Here is the code for the WelcomeScreen:

import 'package:ctfultterfireexperiments/src/screens/home_screen.dart';
import 'package:ctfultterfireexperiments/src/screens/login_signup_screen.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';

class WelcomeScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<FirebaseUser>(
      stream: FirebaseAuth.instance.onAuthStateChanged,
      builder: (BuildContext _, AsyncSnapshot<FirebaseUser> snapshot) {
        //if the snapshot is null, or not has data it is signed out
        if(! snapshot.hasData) return LoginSignupScreen();
        // if the snapshot is having data it is signed in, show the homescreen
        return HomeScreen();
      },
    );
  }
}

Here is the code for HomeScreen.dart

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

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          child: Center(
              child: FutureBuilder(
            builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot) {
              if(!snapshot.hasData) return LinearProgressIndicator();
              return Text("Home Screen: ${snapshot.data.displayName}");
            },
            future: FirebaseAuth.instance.currentUser(),
          )),
        ),
        Spacer(),
        RaisedButton(onPressed: () {FirebaseAuth.instance.signOut();})
      ],
    );
  }
}

Here is the code for LoginSignupScreen.dart:

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

class LoginSignupScreen extends StatelessWidget {

  login() async{
    final GoogleSignIn _googleSignIn = GoogleSignIn();
    final _auth = FirebaseAuth.instance;
    final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
    final GoogleSignInAuthentication googleAuth = await googleUser.authentication;

    final AuthCredential credential = GoogleAuthProvider.getCredential(
      accessToken: googleAuth.accessToken,
      idToken: googleAuth.idToken,
    );

    final FirebaseUser user = (await _auth.signInWithCredential(credential)).user;
    print("signed in " + user.displayName);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Spacer(flex: 1,),
        Text("Login/Signup Screen"),
        Spacer(flex: 2,),
        RaisedButton(onPressed: login)
      ],
    );
  }
}

This will work as a minimum working example.

Raveesh Agarwal
  • 168
  • 1
  • 8