bizz84 / firebase_auth_demo_flutter

Reference Authentication Flow with Flutter & Firebase
https://codewithandrea.com/
MIT License
674 stars 178 forks source link

How to handle StreamBuilder rebuilds resulting from navigation? #32

Closed narciero closed 5 years ago

narciero commented 5 years ago

Thanks for a great demo! I'm running into an issue trying to incorporate navigation after the user has successfully logged in.

Problem

When I call Navigator.push(context, <any_route>) it causes the LandingPage widget tree to be rebuilt, which then recreates the StreamBuilder<User>. Upon successful login, I changed your code to return an AuthenticatedLandingPage widget instead of HomePage so I can load the user's profile before displaying the home page. Unfortunately, every time I navigate it reloads their profile due to the rebuilding.

Question

Do you have any established best practices for dealing with loading data like this after logging in? I know the build method can be called at anytime it just seems like using StreamBuilder inside of build results in lots of recreations of the User stream (and any subsequent data streams dependent on the User like in my case UserProfile).

Code

// LandingPage.dart

class LandingPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<FirebaseUser>(
      stream: FirebaseAuth.instance.onAuthStateChanged,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.active) {
          if (!snapshot.hasData) {
            return AuthSplashPage();
          }

          final user = snapshot.data;
          return Provider<FirebaseUser>.value(
            value: user,
            child: const AuthenticatedLandingPage(),
          );
        }

        return _loadingScreen("Authenticating");
      },
    );
  }
}

class AuthenticatedLandingPage extends StatelessWidget {
  const AuthenticatedLandingPage();

  @override
  Widget build(BuildContext context) {
    // this gets rebuilt every time we navigate in the app
    final user = Provider.of<FirebaseUser>(context);
    final userService = Provider.of<UserService>(context);

    return StreamBuilder<UserProfile>(
      stream: userService.streamProfile(user.uid), // calls Firestore.instance.collection("user_profile").document(user.uid).snapshots()
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.active) {
          if (!snapshot.hasData) {
            return OnboardingSplashPage();
          }

          final profile = snapshot.data;
          return Provider<UserProfile>.value(
            value: profile,
            child: const HomePage(),
          );
        }

        return _loadingScreen("Loading profile");
      },
    );
  }
}

Thanks!

bizz84 commented 5 years ago

@narciero thanks for mentioning this.

I have encountered this problem in various projects, however I haven't found a good solution yet. One idea worth exploring is to convert the LandingPage into a StatefulWidget that holds a subscription to the onAuthStateChanged stream.

This way, a local FirebaseUser variable can be stored and updated when the user changes. And this may reduce unnecessary StreamBuilder rebuilds.

NOTE: I'm mentioning this as something worth exploring - I haven't tested it in practice.

bizz84 commented 5 years ago

@narciero Take a look at the latest changes: #39.

With this setup, the auth state StreamBuilder has been moved above the MaterialApp, and I no longer see the rebuilds. I have tested that this works when pushing a new route, as well as showing the on-screen keyboard.

Closing - but let me know if you have more questions

giorgio79 commented 4 years ago

Another solution mentioned here is using a Stateful widget https://stackoverflow.com/questions/56835372/flutter-stream-builder-triggered-when-navigator-pop-or-push-is-called