Flutter: 在 Widget 构建完成后运行方法

334

我希望能在小部件构建/加载完成后运行函数,但我不确定如何做到。

我的当前用例是检查用户是否已经通过身份验证,如果没有,则重定向到登录视图。我不想在之前进行检查并推送登录视图或主视图,它需要在主视图加载完成后发生。

有什么方法可以实现这一点吗?


2
你不太可能想在“build”中开始登录过程。Build可以在任何时候被多次调用。 - Günter Zöchbauer
看这个:https://dev59.com/Y6nka4cB1Zd3GeqPVuqP - shadowsheep
在我的情况下,我希望启动画面保持显示,直到我决定用户是否已登录,这样我才能将他们带过登录界面。这一点证明非常困难,因为很难确定登录界面之后的下一个界面是否已经渲染完成!... - Karolina Hagegård
15个回答

406
你可以使用 https://github.com/slightfoot/flutter_after_layout 这个库,它可以在布局完成后仅执行一次函数。 或者查看其实现并将其添加到您的代码中 :-) 基本上就是这样。
  void initState() {
    super.initState();
    WidgetsBinding.instance
        .addPostFrameCallback((_) => yourFunction(context));
  }

17
“WidgetsBinding.instance.addPostFrameCallback((_) => yourFunction(context));”这个方法不再可用。 - Pablo Insua
2
@anunixercoder:这取决于您的用例。有时,您应该在initState之外的其他地方调用它,例如在build中。 - Giraldi
2
你应该在 yourFunction 方法内调用 setState 以使其正常工作。 - Pars
1
在代码中频繁使用 WidgetsBinding.instance.addPostFrameCallback 是否表示不良实践? - Syed Mushaheed
1
@Thomas,这个解决方案对我来说完全没有问题。非常感谢。 - Sohail Ansari
显示剩余4条评论

152

更新: Flutter v1.8.4

两种提到的代码现在都可以正常工作:

正常工作:

WidgetsBinding.instance
        .addPostFrameCallback((_) => yourFunction(context));

工作中

import 'package:flutter/scheduler.dart';

SchedulerBinding.instance.addPostFrameCallback((_) => yourFunction(context));

1
第二个不再起作用。NoSuchMethodError (NoSuchMethodError: 方法'addPostFrameCallback'在空上被调用。 接收器:null - Oliver Dixon
1
@EliaWeiss - 这取决于你的使用情况 - 这只是一种在构建后调用小部件函数的方法。典型的用法将在init()中。 - anmol.majhail
我正在尝试从initState调用一个使用localStorage值的函数。我调用WidgetsBinding.instance.addPostFrameCallback((_) => _authenticateWithLocalStorage()); 当localStorage正在初始化时,我会遇到绑定错误。 - Golden Lion
从调用范围捕获的“context”可能在调用后帧回调时无效(失效),这可能会导致异常(例如,如果您尝试访问context.state,而在布局后state已被清空)。 - Luke Hutchison

96

最佳实现方式:

1. WidgetsBinding

WidgetsBinding.instance.addPostFrameCallback((_) {
      print("WidgetsBinding");
    });

2. SchedulerBinding

SchedulerBinding.instance.addPostFrameCallback((_) {
  print("SchedulerBinding");
});

它可以在initState内部调用,两者仅在构建小部件完成渲染后调用一次。

@override
  void initState() {
    // TODO: implement initState
    super.initState();
    print("initState");
    WidgetsBinding.instance.addPostFrameCallback((_) {
      print("WidgetsBinding");
    });
    SchedulerBinding.instance.addPostFrameCallback((_) {
      print("SchedulerBinding");
    });
  }

以上两个代码将会以同样的方式工作,因为它们都使用了相似的绑定框架。


3
如果它是一个StatelessWidget,会怎么样? - Sneha Mudhigonda

66

有三种可能的方法:

1) WidgetsBinding.instance.addPostFrameCallback((_) => yourFunc(context));

2) Future.delayed(Duration.zero, () => yourFunc(context));

3) Timer.run(() => yourFunc(context));

context 而言,我需要在所有 widget 渲染完成后使用 Scaffold.of(context)

但我个人认为,最好的方法是这样的:

void main() async {
  WidgetsFlutterBinding.ensureInitialized(); //all widgets are rendered here
  await yourFunc();
  runApp( MyApp() );
}

4
在Flutter的GetX框架中,第二种方式更受推荐(在小部件声明内部):Future.delayed(Duration.zero, () => yourFunc(context)); - Constantine Kurbatov
2
我可以确认@ConstantineKurbatov的说法。使用GetX和WidgetsBinding并没有起作用,反而产生了错误的结果和奇怪的行为。使用Future.delayed()解决了我的问题! - Jonathan Rhein
嗨,@JonathanRhein,我在一个项目中确切地使用了第一种选择,并且没有生成任何错误,你能否更详细地解释一下你遇到的错误? - Felipe Sales

14

Flutter 1.2 - dart 2.2

根据官方指南和来源,如果您想确保您布局的最后一帧也被绘制出来,可以写以下代码:

import 'package:flutter/scheduler.dart';

void initState() {
   super.initState();
   if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
        SchedulerBinding.instance.addPostFrameCallback((_) => yourFunction(context));
   }
}

对我来说,这不起作用,因为在initState()时间我获得的schedulerPhase的值是_SchedulerPhase.idle_...实际上起作用的是在build()中添加该检查。 - Alessio
2
尝试以下方式:Widget build(BuildContext context) { Future.delayed(Duration.zero,() {//在完成时执行某些操作}); return Scaffold() }; - Constantine Kurbatov

14

在Flutter 1.14.6版本和Dart 28版本中。

以下是对我有效的方法,您只需要将构建方法之后想要发生的一切都捆绑到一个单独的方法或函数中即可。

@override
void initState() {
super.initState();
print('hello girl');

WidgetsBinding.instance
    .addPostFrameCallback((_) => afterLayoutWidgetBuild());

}

13
如果你正在寻找 ReactNative 的 componentDidMount 等效方法,Flutter 也有类似的功能。虽然不是那么简单,但它的工作方式与 React Native 相同。在 Flutter 中,Widget 不直接处理其事件,而是使用其 State 对象来完成。
class MyWidget extends StatefulWidget{

  @override
  State<StatefulWidget> createState() => MyState(this);

  Widget build(BuildContext context){...} //build layout here

  void onLoad(BuildContext context){...} //callback when layout build done
}

class MyState extends State<MyWidget>{

  MyWidget widget;

  MyState(this.widget);

  @override
  Widget build(BuildContext context) => widget.build(context);

  @override
  void initState() => widget.onLoad(context);
}

State.initState 会在屏幕完成渲染布局后立即调用。如果您处于调试模式下,即使进行热重载,在显式到达时刻之前也不会再次调用 State.initState


从我的示例中,您可以使用StatefulWidget类来处理其State对象,就像使用StatelessWidget一样,但我强烈不建议这样做。虽然我还没有发现任何问题,但请先尝试在State对象内部实现所有内容。 - stackunderflow
2
Google建议在initState()中调用数据获取。因此,这种解决方案没有问题,实际上应该是被接受的答案。 - Developer
在React Native中,数据获取可以在布局渲染之前的componentWillMount中完成。Flutter提供了更简单的解决方案。如果我们知道如何正确地执行它,initState足以同时完成数据获取和布局渲染。 - stackunderflow
1
componentWillMount 方法即将被弃用。因此,获取数据将在组件挂载和构造完成后进行。 - Developer
这看起来是一个非常不传统的做法!为什么你把状态的构建方法放在小部件头部,而不是像通常那样放在状态内部? - Karolina Hagegård

8

PostFrameCallback会在屏幕完全绘制之前触发,因此Devv的答案很有帮助,需要添加延迟以允许屏幕绘制。

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
       Future.delayed(Duration(seconds: 3), () => yourFunction());
    });
  }

2
添加例如3秒的问题在于你实际上并不知道它是否需要3秒!而且在不同设备上可能会有所不同!...肯定有一种方法可以让程序自己告诉你什么时候绘制完成。 - Karolina Hagegård

6

如果您在使用新的SDK时遇到了与旧答案不兼容的问题,您可以尝试我的解决方案。我已在v3.0.4上测试过。

WidgetsBinding.instance.endOfFrame.then(
  (_) {
    if (mounted) {
          // do some suff 
          // you can get width height of specific widget based on GlobalKey
       };
  },
);

5
尝试使用SchedulerBinding。
 SchedulerBinding.instance
                .addPostFrameCallback((_) => setState(() {
              isDataFetched = true;
            }));

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