Search code examples
flutterdarttreewidgetbuilder

Rebuilding app with StreamBuilder (beginner) | Flutter


This is my widget tree:

Widget tree

I have StreamBuilder (PURPLE) which is re-building app after clicking FloatingActionButton (RED).

Where the problem is

StreamBuilder rebuilds only objects under him, that means UserSettingsPage (GREEN) isn't getting rebuild. I want him to be rebuild.

My thoughts about fixing it

1.) I think I can maybe move my StreamBuilder above MateriallApp, but I don't know how to implement it.

2.) Maybe i can somehow move UserSettingsPage next to HomePage

I am open for every solution.

(main.dart)

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MultiProvider(providers: [
    ChangeNotifierProvider(create: (_) => GoogleSignInProvider()),
    ChangeNotifierProvider(create: (_) => ThemesProvider()),
    ChangeNotifierProvider(create: (_) => ZmienneClass())
  ], child: const MyApp()));
}

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

  @override
  Widget build(BuildContext context) => MaterialApp(
        debugShowCheckedModeBanner: false,
        theme: Provider.of<ThemesProvider>(context).darkModeOn
            ? ThemeOptions.white
            : ThemeOptions.black,
        home: Scaffold(
          body: StreamBuilder(
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(child: CircularProgressIndicator());
              } else if (snapshot.hasData) {
                //// 2 OPCJA - DODAĆ TU NAVIGATOR I PRZECZYTAĆ TO
                return const HomePage();
              } else if (snapshot.hasError) {
                return const Center(child: Text("Something went Wrong.."));
              } else {
                return const LoginPage();
              }
            },
            stream: FirebaseAuth.instance.authStateChanges(),
          ),
        ),
      );
}

I am opening UserSettingsPage via clicking on FloatingActionButton (RED) which is in UserPage.

Here is his code :

 onPressed: () {
            Navigator.push(context,
                MaterialPageRoute(builder: (context) => const UserSettingsPage()));
          },

(HomePage)

import 'package:flany/userpage.dart';
import 'package:flany/widgets/game.dart';
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key, firTime, secTime}) : super(key: key);
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _selectedIndex = 1;

  @override
  Widget build(BuildContext context) {
    var screens = [
      Container(),
      Game(),
      const UserPage(),
    ];
    return Scaffold(
        bottomNavigationBar: BottomNavigationBar(
            onTap: (index) {
              setState(() {
                _selectedIndex = index;
              });
            },
            items: const <BottomNavigationBarItem>[
              BottomNavigationBarItem(icon: Icon(Icons.score), label: ""),
              BottomNavigationBarItem(
                  icon: Icon(Icons.games_rounded), label: ""),
              BottomNavigationBarItem(icon: Icon(Icons.person), label: ""),
            ],
            currentIndex: _selectedIndex),
        body: IndexedStack(index: _selectedIndex, children: screens));
  }
}

(UserPage)

import 'package:flany/settingspage.dart';
import 'package:flutter/material.dart';

class UserPage extends StatefulWidget {
  const UserPage({Key? key}) : super(key: key);

  @override
  _UserPageState createState() => _UserPageState();
}

class _UserPageState extends State<UserPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
              context,
              MaterialPageRoute(
                  builder: (context) => const UserSettingsPage()));
        },
        child: const Icon(
          Icons.settings,
        ),
      ),
      body: ListView(
        children: const [
          // SOME WIDGETS
        ],
      ),
    );
  }
}

(UserSettingsPage)

class UserSettingsPage extends StatefulWidget {
  const UserSettingsPage({Key? key}) : super(key: key);

  @override
  _UserSettingsPageState createState() => _UserSettingsPageState();
}

class _UserSettingsPageState extends State<UserSettingsPage> {
// I ADDED THOSE VARIABLES
    final provider = Provider.of<GoogleSignInProvider>(context, listen: true);

    final _uLoggedWithGoogle =
        Provider.of<GoogleSignInProvider>(context).isLoggedWithGoogle;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(children: [
// I ADDED CHECKING LOGIN STATUS, ITS WORKING. ITS UPDATING BUT //NOTHING ELSE HAPPENS
Text('You are logged ${_uLoggedWithGoogle() ? 'in' : 'out'}'),
        // BUTTON WHO IS EXECUTING STREAMBUILDER (onpressed code of it)
        onPressed: () {
          final provider =
             Provider.of<GoogleSignInProvider>(context, listen: false);
          provider.logout();
        },
      ]),
    );
  }
}

(GoogleSignInProvider)

class GoogleSignInProvider extends ChangeNotifier {
// LOGOWANIE PRZEZ EMAIL I HASŁO LOGOWANIE PRZEZ EMAIL I HASŁO

  bool isLoggedWithGoogle() {
    var provUser = FirebaseAuth.instance.currentUser;
    if (provUser?.providerData[0].providerId == "google.com") {
      return true;
    } else {
      return false;
    }
  }

  Future logout() async {
    var provUser = FirebaseAuth.instance.currentUser;
    if (provUser!.providerData[0].providerId == "google.com") {
      await googleSignIn.disconnect();
    }
    FirebaseAuth.instance.signOut();
    isLoggedWithGoogle() == false;

    notifyListeners();
  }}

Solution

  • If you want the UserSettingsPage() to update while looking at it, in response to a button press on that same screen, which triggers a change in login status, here is one way to solve that using a Provider (as I can see you are already using one on this page):

    // The way it is written right now, this could just as well be a
    // StatelessWidget, but I'm keeping it Stateful since that's what you wrote.
    class UserSettingsPage extends StatefulWidget {
      const UserSettingsPage({Key? key}) : super(key: key);
    
      @override
      _UserSettingsPageState createState() => _UserSettingsPageState();
    }
    
    class _UserSettingsPageState extends State<UserSettingsPage> {
      @override
      Widget build(BuildContext context) {
        // Declare your provider first thing in the build method, and make it listen:
        final provider = Provider.of<GoogleSignInProvider>(context, listen: true);
        return Scaffold(
          body: ListView(children: [
            // Below, I have used your provider to change the output on the screen.
            // Since I have not seen your provider file, I don't know how it works,
            // but I assume there is a way for it to tell if the user is logged in
            // or not! :) Plz change your code to the correct way. This is just
            // an example:
            Text('You are logged ${provider.loggedIn ? 'in' : 'out'}'),
            // BUTTON WHO IS EXECUTING STREAMBUILDER
            FloatingActionButton(
              onPressed: () {
              provider.logout();
            },
            )
          ]),
        );
      }
    }
    

    My provider file used above would look something like this:

    class GoogleSignInProvider extends ChangeNotifier {
      bool loggedIn = false;
    
      void login() {
        googleObject.login();
        loggedIn = true;
        notifyListeners();
      }
    
      void logout() {
        googleObject.logout();
        loggedIn = false;
        notifyListeners();
        // The above will notify your UserSettingsPage that 
        // loggedIn has changed to false. It will update its
        // UI accordingly.
      }
    }
    

    Do tell me how it goes!


    Edit

    Now, I believe I have understood that with "rebuild", you don't mean rebuild that screen (as I showed above) but rebuild the app from the first screen, popping any new screens that may have been pushed on top, such as the UserSettingsPage().

    There are two ways to do this.

    Method 1

    If you want the StreamBuilder to be in charge of the popping, you write it like this:

          body: StreamBuilder(
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(child: CircularProgressIndicator());
              } else if (snapshot.hasData) {
                //// 2 OPCJA - DODAĆ TU NAVIGATOR I PRZECZYTAĆ TO
                return const HomePage();
              } else if (snapshot.hasError) {
                return const Center(child: Text("Something went Wrong.."));
              } else {
                // The below command will pop all screens that may have been pushed
                // on top of this one, every time this part of the if statement is triggered:
                Navigator.popUntil(context, (route) => route.isFirst);
                // It will pop screens until it reaches the first screen.
                // You can put this in other parts of the StreamBuilder as well, every time
                // you want the app to start over from the first screen.
    
                return const LoginPage();
              }
            },
            stream: FirebaseAuth.instance.authStateChanges(),
          ),
    

    Method 2

    Another method is to pop the UserSettingsPage() from inside the Log Out button:

            onPressed: () {
              provider.logout();
              Navigator.pop(context);
            },
    

    The latter means you have to put the pop command in EVERY place where you want a screen to pop, whereas putting it in the StreamBuilder means having all the pops in one place. Which one to chose depends on your preferences!

    Explanation

    I believe part of your problem with this is that you are confused about what a screen is in Flutter. You think of your HomePage() and your LoginPage() as two different screens, correct? Well, they are not! The way you have written your code, those are just two different widgets, both displayed on your first screen under different circumstances.

    Your UserSettingsPage() on the other hand, THAT ONE is a new screen! This new screen is placed on top of the first screen in a so-called "stack".

    How do I know this?

    Well, it is the Navigator that decides if something is a new screen or not. This command Navigator.push which you used to get to your UserSettingsPage() created a new screen. If you don't use this command, you will still be on the same screen as before.

    When you say that you want the StreamBuilder to "rebuild the app", I think you mean that you want it to rebuild the app from the first screen. You say that "nothing else happens" but it actually does! I'm sure your first screen has changed from the HomePage() widget to the LoginPage() widget just as you wanted! The reason you don't see it is that your second screen is still on top, covering your first screen. But you can see it by tapping your "Back" button on your phone, since this will pop that screen.

    My solutions above just pop it automatically.