8

I have a stream builder in the app's home/root page. This stream builder gets triggered whenever I do a page-navigation elsewhere, which has nothing to do with the stream itself.

My understanding, according to here and here, is when a page is popped/pushed in the navigator, it triggers a rebuild on the app, so the stream builder gets re-attached and so it fires. However this seems inefficient, so is there a way to prevent the stream builder from firing when a page is popped/pushed?

Additionally, according to the logs, when I push a page, the page is built and shown first, then the stream builder gets fired. However the stream builder's widget/page does not show at all, even though clearly the logs/debugger show that the stream builder's widget has been returned. Where did it go? How does it work in the Flutter framework?

Below is the full code and logs. The code uses Firebase auth as a the stream builder.

Code:

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: AppHomePage(),
    );
  }
}

class AppHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final FirebaseAuth auth = FirebaseAuth.instance;
    return StreamBuilder<FirebaseUser>(
      stream: auth.onAuthStateChanged,
      builder: (_, AsyncSnapshot<FirebaseUser> snapshot) {
        if (snapshot.connectionState == ConnectionState.active) {
          final FirebaseUser user = snapshot.data;
          if (user == null) {
            debugPrint("User is NULL.");
            return SignInPage();
          } else {
            debugPrint("User exists.");
            return MainPage();
          }
        } else {
          debugPrint("In waiting state.");
          return Scaffold(
            body: Center(
              child: CircularProgressIndicator(),
            ),
          );
        }
      },
    );
  }
}

class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    debugPrint("Building main page.");
    return Scaffold(
      body: Center(
        child: Text("Welcome to our app!"),
      ),
    );
  }
}

class SignInPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    debugPrint("Building sign-in page.");
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            FlatButton(
              color: Colors.blue,
              child: Text('Sign In as Anonymous'),
              onPressed: () {
                debugPrint("Anonymous");
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => MainPage()),
                );
              },
            ),
            FlatButton(
              color: Colors.red,
              child: Text('Sign In with Google'),
              onPressed: () => debugPrint("Google"),
            ),
          ],
        ),
      ),
    );
  }
}

Logs, where the 4th line indicates a button is pressed to do a navigator.pop():

I/flutter (22339): In waiting state.
I/flutter (22339): User is NULL.
I/flutter (22339): Building sign-in page.
I/flutter (22339): Anonymous
I/flutter (22339): Building main page.
I/flutter (22339): User is NULL.
I/flutter (22339): Building sign-in page.
henrykodev
  • 2,964
  • 3
  • 27
  • 39

5 Answers5

7

I can confirm that the build method in StreamBuilder is called every time we navigate within our app, which is not efficient since it should cancel its listener, create a new one and rebuild the entire widget.

You may face that issue if your app listens to the authentication state in order to show an appropriate screen when auth state changes (Loading/Login/Home)

So in most of the tutorials, you will see that StreamBuilder is created in the build method in a Stateless widget. This is not an efficient solution.

Instead use Stateful widget and listen to your auth changes in initState() or didChangeDependencies() methods.

The difference in our case would be that in initState() you will have issues in getting your Auth service if you use Provider (The context won't be ready with the Provided service yet). If you don't use Provider you can listen to changes in the initState(). But I highly recommend using Provider to separate your Services and Pages. In other words, use the MVVM pattern so your code will be scalable and maintainable.

class LandingScreen extends StatefulWidget {
  @override
  _LandingScreenState createState() => _LandingScreenState();
}

class _LandingScreenState extends State<LandingScreen> {
  @override
  Widget build(BuildContext context) {
      return SplashView();
  }

  @override
  void didChangeDependencies() {
      //we don't have to close or unsubscribe SB
        Provider.of<AuthService>(context, listen: false).streamAuthServiceState().listen((state){
          switch (state) {
            case AuthServiceState.Starting:
            print("starting");
              break;
            case AuthServiceState.SignedIn:
              Navigator.pushReplacementNamed(context, Routes.HOME);
              break;
            case AuthServiceState.SignedOut:
              Navigator.pushReplacementNamed(context, Routes.LOGIN);
              break;
            default:
              Navigator.pushReplacementNamed(context, Routes.LOGIN);
          }
        });

    super.didChangeDependencies();
  }
}

If you'll use directly Firebase stream - replace my stream with FirebaseAuth.instance.onAuthStateChanged

Kirill Karmazin
  • 6,256
  • 2
  • 54
  • 42
  • Thank you for the thorough explanation and interesting approach. – henrykodev Nov 12 '19 at 10:12
  • Thank you for this info! Would you have an example for listening events in init? I use globals instead of Provider... Globals seem much easier than Provider... – giorgio79 Aug 17 '20 at 14:40
  • By using this it is giving the unmounted error on navigating. Unhandled Exception: This widget has been unmounted, so the State no longer has a context (and should be considered defunct). Do u have any idea? – Gautam Goyal Feb 19 '22 at 12:02
4

I spend hours figuring out how to fix this. Turns out AppHomePage need to extend StatefulWidget instead of StatelessWidget.

No idea why, but it works.

Eliya Cohen
  • 10,716
  • 13
  • 59
  • 116
  • I spent hours too. I will give this solution a try when I get home. Very strange to use a stateful widget. If someone can explain or link to some resources that would be great. – henrykodev Jul 01 '19 at 23:25
2

In AppHomePage StatelessWidget wrap your StreamBuilder under Scaffold widget, then it will not be Triggered when Navigator Pop or Push is Called.

shubham goel
  • 83
  • 1
  • 7
0

Consider to use a statefull widget and load your stream inside initState and use its value during build method, this prevents stream to be rebuild when, for example, you pop and come back from a previously open route

Michael Cauduro
  • 185
  • 1
  • 3
  • 16
-1

I faced the same issue in my HomeScreen which is a StatefulWidget and it was fixed after adding 'const' keyword.

Scaffold(
      bottomNavigationBar: BottomNavigationBar(
          type: BottomNavigationBarType.fixed,
          fixedColor: Colors.black,
          currentIndex: _selectedIndex,
          selectedFontSize: 10.0,
          unselectedFontSize: 10.0,
          onTap: (i) => setState(() {
                _selectedIndex = i;
              }),
          items: const [
            BottomNavigationBarItem(
                icon: Icon(Icons.home_outlined),
                activeIcon: Icon(Icons.home),
                label: 'Home'),
            BottomNavigationBarItem(
                icon: Icon(Icons.explore_outlined),
                activeIcon: Icon(Icons.explore),
                label: 'Explore')
  ]),
      body: Stack(
        children: _screens.asMap().map((i, screen) => MapEntry(i, Offstage(
          offstage: _selectedIndex !=i,
          child: screen,
        ))).values.toList(),
      ),
      drawer: CustomDrawer(),
    );

final _screens = [
       const HomeScreen(),
       Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.white,
          elevation: 0,
          leading: IconButton(
            iconSize: 30,
            color: Colors.black,
             icon: const Icon(Icons.close),
            onPressed: () => _navigateTo(0),
           ),),
        body: const Center(
          child: Text('Explore'),
        ),
      ),
chk.buddi
  • 554
  • 1
  • 8
  • 29