Search code examples
flutterstream-builder

StreamBuilder child update not rendering after navigation


I'm using flutter's StreamBuilder to decide which "root" page to show a user. Right now there are basically 2 possibilities - LoginPage or HomePage. My app's main build method looks like this:

  Widget build(BuildContext context) => StreamProvider.value(
      initialData: CurrentUser.initial,
      value: AuthService().user,
      child: Consumer<CurrentUser>(
          builder: (context, currentUser, _) => MaterialApp(
              home: currentUser.isInitialValue
                  ? Scaffold(
                      body: Center(
                        child: CircularProgressIndicator(),
                      ),
                    )
                  : currentUser.user != null
                      ? MultiProvider(providers: [
                          Provider<User>.value(value: currentUser.user),
                          // NOTE: Any other user-bound providers here can be added here
                        ], child: HomePage())
                      : LoginPage())));

The relevant part of the login page is that it gives you two options:

  1. "Are you a new user" - Sign Up
  2. "Are you returning" - Sign in

When you click one of those buttons, you go to the form in a certain mode via the navigator:

Navigator.push(
                                context,
                                CupertinoPageRoute(
                                  builder: (context) => LoginPage(
                                    mode: LoginPageMode.signUp,
                                  ),
                                ))

After successfully logging in or signing up, the stream gets updated and the HomePage renders. I know this because I have a print statement in the build method ("Building HomePage"):

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

  @override
  Widget build(BuildContext context) {
    print('Building HomePage');
    return Scaffold(
        body: Container(
      alignment: Alignment.center,
      child: Text('HOME'),
    ));
  }
}

However, the screen does not actually change. The screen remains on the login page. I think I'm managing the stream fine because if I wasn't the render method would never be hit. I saw a similar question here but it seems they are not managing the stream properly. How can I see the render method hit, yet the screen stay the same? I've been working with Flutter for a while and I've never seen this before.

I think it has something to do with the navigation because if I remove the step where the user chooses "sign in" or "sign up", and I just send them to the sign in page, the issue disappears. It's like the HomePage is being built under the page that was navigated to.


Solution

  • 1. Explanation

    The reason of such behavior is that you use root Navigator that pushes LoginPage route on the apps root level. Here is widgets tree reveals widgets relations after push:

    App
      |
      |-- StreamBuilder
      |      |--LoginPage()
      |
      |-- LoginPage(mode: LoginPageMode.signUp)
    
    

    So when your StreamBuilder changes data the tree becomes this:

    App
      |
      |-- StreamBuilder
      |      |--HomePage()    // <--- CHANGED
      |
      |-- LoginPage(mode: LoginPageMode.signUp)
    
    

    Thats why you still see LoginPage and at the same time HomePage rendered too. HomePage just lays "under" LoginPage.

    2. Solution

    The solution is to use nested Navigator:

    
    Widget build(BuildContext context) => StreamProvider.value(
          initialData: CurrentUser.initial,
          value: AuthService().user,
          child: Consumer<CurrentUser>(
              builder: (context, currentUser, _) => MaterialApp(
                  home: currentUser.isInitialValue
                      ? Scaffold(
                          body: Center(
                            child: CircularProgressIndicator(),
                          ),
                        )
                      : currentUser.user != null
                          ? MultiProvider(providers: [
                              Provider<User>.value(value: currentUser.user),
                              // NOTE: Any other user-bound providers here can be added here
                            ], child: HomePage())
                          : Navigator(                //  <--- HERE
                              onGenerateRoute: (settings) {
                                return CupertinoPageRoute(
                                  builder: (context) => LoginPage(),
                                );
                              },
                            )));
    
    ...
    
    

    In that case when you will call Navigator.push(context, ...) inside LoginPage your widget tree will look like this:

    App
      |
      |-- StreamBuilder
             |-- Navigator()
                  |-- LoginPage()
                  |-- LoginPage(mode: LoginPageMode.signUp)
    
    

    Please try this approach, it should work.