Flutter Stream Builder 当 Navigator Pop 或 Push 被调用时触发

8

我在应用程序的主/根页面中有一个流构建器。无论何时我在其他地方进行页面导航,这个流构建器都会触发,而与流本身无关。

据我理解,根据这里这里的说法,当从导航器中弹出/推送页面时,它会触发应用程序的重建,因此流构建器会重新附加并触发。但是这似乎效率低下,是否有一种方法可以防止在推出或推入页面时触发流构建器?

此外,根据日志,在我推送页面时,页面首先被构建和显示,然后流构建器得到触发。但是,尽管日志/调试器清楚地表明已经返回了流构建器的小部件/页面,但流构建器的小部件/页面根本没有显示。它去哪儿了?Flutter框架如何工作?

以下是完整的代码和日志。该代码使用Firebase auth 作为流构建器。

代码:

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"),
            ),
          ],
        ),
      ),
    );
  }
}

日志,第四行表示按下按钮执行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.
5个回答

7
我可以确认StreamBuilder中的build方法在每次导航时都会被调用,这并不是高效的做法,因为它应该取消其监听器、创建新的监听器并重建整个widget。
如果您的应用程序侦听身份验证状态以在身份验证状态更改时显示适当的屏幕(加载/登录/主页),则可能会遇到此问题。
因此,在大多数教程中,您会看到StreamBuilder是在Stateless widget的build方法中创建的。这不是一个高效的解决方案。
相反,使用Stateful widget并在initState()或didChangeDependencies()方法中监听您的身份验证更改。
在我们的情况下,区别在于在initState()中,如果使用Provider获取Auth服务,您将无法获取它(提供服务的上下文还没有准备好)。如果您不使用Provider,您可以在initState()中监听更改。但我强烈建议使用Provider来分离您的服务和页面。换句话说,使用MVVM模式,这样您的代码将具有可扩展性和可维护性。
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();
  }
}

如果您直接使用Firebase流,请将我的流替换为FirebaseAuth.instance.onAuthStateChanged


感谢您的详细解释和有趣的方法。 - henrykodev
谢谢您提供这些信息!您有在init中监听事件的示例吗?我使用全局变量而不是Provider... 全局变量似乎比Provider更容易... - giorgio79
使用它进行导航时,会出现未安装错误。 未处理的异常:此小部件已被卸载,因此状态不再具有上下文(应视为失效)。你有任何想法吗? - Gautam Goyal

4

我花了数小时来解决这个问题。结果发现AppHomePage需要扩展StatefulWidget而不是StatelessWidget

不知道为什么,但它有效。


我也花了好几个小时。等我回家后,我会尝试这个解决方案。使用有状态的小部件很奇怪。如果有人可以解释或提供一些资源链接,那就太好了。 - henrykodev

2
在 AppHomePage StatelessWidget 中,将您的 StreamBuilder 包装在 Scaffold widget 下面,这样当调用 Navigator Pop 或 Push 时,它就不会被触发。

0
考虑使用有状态的小部件,在initState中加载流并在build方法中使用其值,这可以防止流在例如从先前打开的路由弹出并返回时重新构建。

-1

我在我的HomeScreen中遇到了相同的问题,它是一个StatefulWidget,在添加“const”关键字后问题解决了。

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'),
        ),
      ),

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接