【问题标题】:Flutter StatefulWidget causing multiple HTTP re-requestsFlutter StatefulWidget 导致多个 HTTP 重新请求
【发布时间】:2019-12-06 01:15:21
【问题描述】:

这可能是由于我正在做或忘记做的一些愚蠢的事情造成的。 以下是 Flutter 的完整工作代码示例(截至 2019 年 12 月 5 日)和用于重现此问题的 HTTP 回显服务器(httpbin)。

运行httpbin:

docker run -p 1234:80 kennethreitz/httpbin

然后将代码加载到新的 Flutter 应用中。 在重新加载应用程序时,单击抽屉中的 Route A,您会在控制台中打印以下内容:

flutter: Loaded <RouteA> (Stateful)
flutter: Got data from Route A 1 times.

点击Route B,你会得到:

flutter: Loaded <RouteB> (Stateful)
flutter: Got data from Route B 1 times.
flutter: Loaded <RouteA> (Stateful)
flutter: Got data from Route A 2 times.

(它重新加载路由 A,执行另一个 HTTP 请求)。

再次加载Route B,你会得到:

flutter: Loaded <RouteB> (Stateful)
flutter: Got data from Route B 2 times.
flutter: Loaded <RouteB> (Stateful)
flutter: Loaded <RouteA> (Stateful)
flutter: Got data from Route A 3 times.
flutter: Got data from Route B 3 times.

再次加载Route B,你会得到:

flutter: Loaded <RouteB> (Stateful)
flutter: Got data from Route B 4 times.
flutter: Loaded <RouteB> (Stateful)
flutter: Loaded <RouteA> (Stateful)
flutter: Loaded <RouteB> (Stateful)
flutter: Got data from Route B 5 times.
flutter: Got data from Route B 6 times.
flutter: Got data from Route A 4 times.

这些负载中的每一个都对应一个 HTTP 请求,因此,如果应用已打开足够长的时间,它可能会为单个有状态小部件负载发出 100 个 HTTP 请求。

请注意,如果您加载 Route C(一个无状态小部件),它只会加载一次。

这显然与 StatefulWidgets 的重新加载方式有关,但我被卡住了,无法在网上找到有类似问题的帖子。

Flutter 为什么要这样做?对于 HTTP 请求,如何使其表现得像 StatelessWidget?

见下面的代码示例

/*
 * Flutter code for weird HTTP behavior with StatefulWidget
 *
 * Make sure you're also running httpbin locally with the following command:
 *
 *   docker run -p 1234:80 kennethreitz/httpbin
 */
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;


int routeAqueries = 0;
int routeBqueries = 0;
int routeCqueries = 0;


void main() {
  runApp(HttpDebug());
}


class HttpDebug extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'HomeDebug',
      home: HomeDebug(),
    );
  }
}


class HomeDebug extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      drawer: _Drawer(),
      body: Center(child: Text('Home')),
    );
  }
}


class RouteA extends StatefulWidget {
  @override
  _RouteAState createState() => _RouteAState();
}

class _RouteAState extends State<RouteA> {
  @override
  Widget build(BuildContext context) {
    print('Loaded <RouteA> (Stateful)');
    return Scaffold(
      appBar: AppBar(title: Text('Route A')),
      drawer: _Drawer(),
      body: FutureBuilder<String>(
        future: fetchRoute('routeA'),
        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
          if (snapshot.hasData) {
            return Text('RouteA Data: ${snapshot.data}');
          } else if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
          } else {
            return Text('Loading');
          }
        },
      ),
    );
  }
}


class RouteB extends StatefulWidget {
  @override
  _RouteBState createState() => _RouteBState();
}

class _RouteBState extends State<RouteB> {
  @override
  Widget build(BuildContext context) {
    print('Loaded <RouteB> (Stateful)');
    return Scaffold(
      appBar: AppBar(title: Text('Route B')),
      drawer: _Drawer(),
      body: FutureBuilder<String>(
        future: fetchRoute('routeB'),
        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
          if (snapshot.hasData) {
            return Text('RouteB Data: ${snapshot.data}');
          } else if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
          } else {
            return Text('Loading');
          }
        },
      ),
    );
  }
}


class RouteC extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('Loaded <RouteC> (Stateless)');
    return Scaffold(
      appBar: AppBar(title: Text('Route C')),
      drawer: _Drawer(),
      body: FutureBuilder<String>(
        future: fetchRoute('routeC'),
        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
          if (snapshot.hasData) {
            return Text('RouteC Data: ${snapshot.data}');
          } else if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
          } else {
            return Text('Loading');
          }
        },
      ),
    );
  }
}


class _Drawer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Drawer(
        child: ListView(
          children: <Widget>[
            ListTile(
              title: Text('Home'),
              onTap: () => Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => HomeDebug()),
              ),
            ),
            ListTile(
              title: Text('Route A'),
              onTap: () => Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => RouteA()),
              ),
            ),
            ListTile(
              title: Text('Route B'),
              onTap: () => Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => RouteB()),
              ),
            ),
            ListTile(
              title: Text('Route C'),
              onTap: () => Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => RouteC()),
              ),
            ),
          ],
        )
    );
  }
}


Future<String> fetchRoute(String route) async {
  Map<String, int> routes = {
    'routeA': 200,
    'routeB': 201,
    'routeC': 202,
  };

  final response = await http.get('http://localhost:1234/status/${routes[route]}');

  if (response.statusCode == 200) {
    print('Got data from Route A ${++routeAqueries} times.');
    return 'Welcome to Route A';
  } else if (response.statusCode == 201) {
    print('Got data from Route B ${++routeBqueries} times.');
    return 'Welcome to Route B';
  } else if (response.statusCode == 202) {
    print('Got data from Route C ${++routeCqueries} times.');
    return 'Welcome to Route C';
  }
}

【问题讨论】:

    标签: flutter


    【解决方案1】:

    原因
    https://medium.com/saugo360/flutter-my-futurebuilder-keeps-firing-6e774830bc2
    重建时,新小部件的 Future 实例与旧的不同

    https://github.com/flutter/flutter/issues/11426#issuecomment-414047398
    每次发出重建时都会调用 FutureBuilder 状态的 didUpdateWidget。此函数检查旧的未来对象是否与新的不同,如果是,则重新启动 FutureBuilder。 为了解决这个问题,我们可以在构建函数之外的其他地方调用 Future。

    https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html
    未来必须更早获得,例如在 State.initState、State.didUpdateConfig 或 State.didChangeDependencies 期间。在构造 FutureBuilder 时,不能在 State.build 或 StatelessWidget.build 方法调用期间创建它。如果future与FutureBuilder同时创建,那么每次FutureBuilder的parent重建时,异步任务都会重新启动。
    一般准则是假设每个构建方法都可以在每一帧被调用,并将省略的调用视为优化。

    解决方案
    https://github.com/flutter/flutter/issues/11426#issuecomment-414047398
    而不是:

    FutureBuilder(
      future: someFunction(),
      ....
    

    我们应该有:

    initState() {
      super.initState();
      _future = SomeFunction();
    }
    

    然后

    FutureBuilder(
      future: _future,
    

    在你的抽屉里,你需要使用pushReplacement

    【讨论】:

    • 感谢您提供讨论此问题的 Github 问题的链接。是否使用 pushReplacement 最佳实践?或者在这种情况下我应该只使用 StatelessWidgets 吗?我遇到了一些帖子,其中人们提到添加记忆/缓存以检查它是否已经加载,这似乎是一个巨大的 hack 以避免 Navigator.push feature 每次都重新加载堆栈中的所有内容一条新路由被推送。
    • 如果你使用 push ,你需要在某处弹出。如果您只是在没有弹出的情况下进行推送。当用户单击设备上的返回按钮时,用户必须单击很多返回按钮才能返回主页。您可以直接使用您的案例进行测试。然后点击返回看看会发生什么。
    • 我不知道如何缓存来检查它是否已经被加载,你能提供给我的链接吗?谢谢。
    • 我发现了这个问题 github.com/flutter/flutter/issues/11655 ,正在进行 PR。可能这个重建会在 PR 登陆后消失。
    • 是的,在某些情况下(例如从主页上的抽屉中导航),使用后退按钮没有意义。我遇到了同样的问题,并得到 Flutter 开发人员不相信这是一个问题的印象。我认为至少可以改进有关此行为的文档,因为我认为这不直观。鉴于那里发布有关该问题的人数,手指交叉已经完成。
    【解决方案2】:

    正如 chunhunghan 所说,获取应该在 initState 而不是 build 方法中进行(this documention 和前面的两个步骤对我理解和修复请求数量很有帮助。)使用信息在那个链接中,我最终为每个有状态小部件提供了这个:

    class _RouteAState extends State<RouteA> {
      Future<String> _post;
    
      @override
      void initState() {
        super.initState();
        _post = fetchRoute('routeA');
      }
    
      @override
      Widget build(BuildContext context) {
        // ...
            future: _post,
    

    如果我理解正确,请求的数量并不是您想要解决的唯一问题。即使将 fetch 移至 initState,您仍然会看到多个 flutter: Loaded &lt;RouteX&gt; (Stateful) 在每次导航时被触发。这是因为所有路由仍在导航器堆栈上,因此有状态路由的构建方法为堆栈上的每个路由运行。看到所需结果的最简单的补丁是将Navigator.push 替换为Navigator.pushReplacement,但您可能需要更详细的内容,以防止返回导航退出应用程序。

    还有很多其他选项可以替换路由,因此请务必查看是否有其他选项更符合您想要的语义。

    【讨论】:

    • 在我的研究中,我从 2017 年 8 月 17 日发现了这个持续存在的问题,并在 7 天前关闭。 github.com/flutter/flutter/issues/11655 我个人觉得很奇怪,每次推送路由时 Flutter 都会重建整个 Navigator 堆栈,以防堆栈上先前访问的页面发生更改。感谢您提供有关将 fetch 置于 initState 中的信息。几个月前,当我阅读该文档时,我一定忽略了该注释。
    猜你喜欢
    • 2019-10-09
    • 2020-05-30
    • 2021-05-03
    • 1970-01-01
    • 2021-05-24
    • 2015-10-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多