在Flutter中从一个有状态的小部件调用另一个有状态的小部件的方法。

125
我有一个有状态的小部件,并在扩展State<MusicPlayer>类的那个有状态的小部件中有一些函数。
文件lib\main.dart 只是简单地使用一个函数。
class MyAppState extends State<MyApp>{
...
void printSample (){
  print("Sample text");
}
...

这个功能是在主类内部的有状态小部件中。
还有另一个文件 lib\MyApplication.dart
这个文件也有一个有状态小部件,我能做些什么来在这里调用函数 printSample() ..
class MyApplicationState extends State<MyApplication>{
...
@override
  Widget build(BuildContext context) {
    return new FlatButton(
      child: new Text("Print Sample Text"),
      onPressed :(){
       // i want to cal the function here how is it possible to call the 
       // function 
       // printSample()  from here??  
      }
    );
  }
}

如果您可以确保一个小部件是另一个小部件的后代,则可以使用InheritedWidget - Jacob Phillips
@JacobPhillips 在那种情况下只需使用 context.ancestorStateOfType - Rémi Rousselet
@RémiRousselet,我需要你的帮助。我正在尝试从子小部件将数据发送到父小部件,但是所有有关流和可监听对象的示例都是反过来的,这正好是我的问题所在。我想要从子小部件调用一个函数到父小部件,很抱歉我之前没有提到这一点。 - Aman Malhotra
1
将整个StreamController传递给子代,而不仅仅是Stream,如果您的子代应该提交事件。 - Rémi Rousselet
9个回答

138

要调用父元素的函数,您可以使用回调模式。在此示例中,将一个函数(onColorSelect)传递给子元素。当按下按钮时,子元素调用该函数:

import 'package:flutter/material.dart';

class Parent extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return ParentState();
  }
}

class ParentState extends State<Parent> {
  Color selectedColor = Colors.grey;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          color: selectedColor,
          height: 200.0,
        ),
        ColorPicker(
          onColorSelect: (Color color) {
            setState(() {
              selectedColor = color;
            });
          },
        )
      ],
    );
  }
}

class ColorPicker extends StatelessWidget {
  const ColorPicker({this.onColorSelect});

  final ColorCallback onColorSelect;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        RaisedButton(
          child: Text('red'),
          color: Colors.red,
          onPressed: () {
            onColorSelect(Colors.red);
          },
        ),
        RaisedButton(
          child: Text('green'),
          color: Colors.green,
          onPressed: () {
            onColorSelect(Colors.green);
          },
        ),
        RaisedButton(
          child: Text('blue'),
          color: Colors.blue,
          onPressed: () {
            onColorSelect(Colors.blue);
          },
        )
      ],
    );
  }
}

typedef ColorCallback = void Function(Color color);

内部的Flutter小部件(例如按钮或表单字段)使用完全相同的模式。如果您只想调用没有参数的函数,则可以使用VoidCallback类型,而不是定义自己的回调类型。


如果您想通知更高层级的父级,则可以在每个层次结构级别上重复此模式:

class ColorPickerWrapper extends StatelessWidget {
  const ColorPickerWrapper({this.onColorSelect});
  
  final ColorCallback onColorSelect;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(20.0),
      child: ColorPicker(onColorSelect: onColorSelect),
    )
  }
}

在Flutter中,不建议从父Widget调用子Widget的方法。相反,Flutter鼓励您将子Widget的状态作为构造函数参数向下传递。而不是调用子项的方法,您只需在父Widget中调用setState来更新其子项。


另一种替代方法是在Flutter中使用controller类(例如ScrollControllerAnimationController等)。这些类也作为构造函数参数传递给子项,并包含控制子项状态的方法,而无需在父项上调用setState。例如:

scrollController.animateTo(200.0, duration: Duration(seconds: 1), curve: Curves.easeInOut);
孩子们随后被要求听取这些变化以更新他们的内部状态。当然,您也可以实现自己的控制器类。如果需要,我建议您查看Flutter的源代码以了解其工作原理。
Futures和streams是传递状态的另一种选择,并且也可以用于调用子级的函数。
但我真的不建议这样做。如果您需要调用子小部件的方法,则非常可能您的应用程序架构存在缺陷。尝试将状态向上移动到公共祖先!

1
从父部件调用子部件的方法是不鼓励的。 即使通过GlobalKey调用,也是不鼓励的吗? - Mohamed Ali
6
有点复杂!! - jrh
是的,谢谢!这真是救了我的命。我有一个从浮动操作按钮调用的大对话框,其中定义了许多本地下拉菜单,我不想复制/粘贴它,但也希望在子状态页面中有相同的浮动操作按钮。这正是我正在寻找的。 - Eradicatore
@boformer 如果我想在不同的类中定义这些类,例如将ColorPicker定义在一个dart文件中,而将(Parent, ParentState)定义在另一个类中,那么我应该在哪里定义typedef ColorCallback呢?我尝试过了,但失败了。 - user12021836
1
如果所用的部件不是继承而来的,而是一个已导入的独立类,那该怎么办呢?在我的情况下,我使用的是 Block 部件,它只是一个有颜色的容器,用于可视化排序算法。当这个块是关键块(插入排序)时,我想改变它的颜色。那么该怎么做呢?如果我已经错过了您之前的回答并重复了问题,请谅解。 - Mrak Vladar
显示剩余4条评论

88
你可以通过使用 widget 的key来完成这个操作。

myWidget.dart

    class MyWidget extends StatefulWidget {
     
      const MyWidget ({Key key}) : super(key: key);
     
      @override
      State<StatefulWidget> createState()=> MyState();
    
  
    }

   class MyState extends State<MyWidget >{
        
          
               
      Widget build(BuildContext context){ return ....}
    
      void printSample (){
         print("Sample text");
      }
        
 }

现在当使用 MyWidget 时,将 GlobalKey 声明为全局键。

  GlobalKey<MyState> _myKey = GlobalKey();

并在创建小部件时传递它

MyWidget(
key : _myKey,
)
通过这个键,你可以调用状态内的任何公共方法。
_myKey.currentState.printSample();

你是不是想说 _myKey.currentState.printSample(); - proto-n
3
由于我的状态小部件名称前面有下划线,例如_MyState,我无法使用GlobalKey并卡住了,改为MyState后问题得到解决。 - J. Diaz
1
@J.Diaz在类名前加下划线表示私有类,对于变量和成员也是如此,因此您无法从目录外访问它。 - Mahmoud Abu Alheja
Widgets库捕获的异常:多个小部件使用了相同的GlobalKey。 - Syed Arsalan Kazmi
@SyedArsalanKazmi 你必须为每个小部件创建对象键。 - Mahmoud Abu Alheja

18

如果您想调用printSample()函数,可以使用:

class Myapp extends StatefulWidget{
...
    final MyAppState myAppState=new MyAppState();
    @override
    MyappState createState() => MyAppState();
    void printSample(){
        myAppState.printSample();
    }
}
class MyAppState extends State<MyApp>{
    void printSample (){
        print("Sample text");
    }
}

...............
Myapp _myapp = new Myapp();
_myapp.printSample();
...

13
我认为这对于更复杂的情况无效:Myapp StatefulWidget 可以被重复构建,因此任何给定实例持有的状态可能不是 Flutter 持有并与树相关联的相同状态对象,对吗? - Pat Niemeyer
你好 @PatNiemeyer ,这样对吗? _StatefulButtonState _lastState; //重写 State createState() { _lastState = new _StatefulButtonState(onPressed: onPressed); return _lastState; } - BuffK
这不是一个好的方法,因为:1.您有方法执行的状态实例和另一个用于Flutter树的状态实例。2.状态可以在小部件生命周期内多次创建。 - Marius Van Nieuwenhuyse

10

你可以尝试这个方法,它将从Page1 (StatefulWidget) widget中调用Page2中定义的一个方法。

class Page1 extends StatefulWidget {
  @override
  _Page1State createState() => _Page1State();
}

class _Page1State extends State<Page1> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: RaisedButton(
          child: Text("Call page 2 method"),
          onPressed: () => Page2().method(),
        ),
      ),
    );
  }
}

class Page2 extends StatefulWidget {
  method() => createState().methodInPage2();

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

class _Page2State extends State<Page2> {
  methodInPage2() => print("method in page 2");

  @override
  Widget build(BuildContext context) => Container();
}

2
这个答案与Mehmet Akif BAYSAL的答案完全相同(概念100%相同)。 Mehmet Akif BAYSAL的答案只是在代码示例中更优雅地说明,因此更容易理解。 - SilSur
如果我想在bloc中调用一个方法,应该怎么做?https://stackoverflow.com/questions/62097660/call-a-method-from-another-class-flutter - Tony

9

使用回调和GlobalKey对于简单的用例是可以的,但对于更复杂的设置,它们应该被视为反模式,因为它们钩入小部件类型并依赖于低级实现逻辑。

如果您发现自己添加了越来越多的回调/全局键,并且变得混乱不堪,那么可能是时候切换到StreamController+StreamSubscription之类的东西了。这样,您就可以将事件与特定的小部件类型解耦,并抽象出小部件间通信逻辑。

注册事件控制器

在您的顶级小部件(根据您的需求,可以是应用级或页面级)中创建 StreamController 实例,并确保在 dispose() 方法中释放它:

class _TopLevelPageState extends State<TopLevelPage> {
  StreamController<MyCustomEventType> eventController = StreamController<MyCustomEventType>.broadcast();

// ...
  @override
  void dispose() {
    eventController.close();
    super.dispose();
  }
}

eventController实例作为构造函数参数传递给需要监听事件或触发事件的任何子部件。

MyCustomEventType可以是枚举(如果您不需要传递额外数据),或者是一个常规对象,其中包含您需要设置的任何字段以便在事件上设置额外数据。

触发事件

现在,在任何小部件中(包括您声明了StreamController的父级小部件),您都可以使用以下代码触发事件:

eventController.sink.add(MyCustomEventType.UserLoginIsComplete);

监听事件

要在您的子(或父)小部件中设置侦听器,请将以下代码放入initState()中:


class _ChildWidgetState extends State<ChildWidget> {
  @override
  void initState() {
    super.initState();
    // NOTE: 'widget' is the ootb reference to the `ChildWidget` instance. 
    this.eventSubscription = widget.eventController.stream.asBroadcastStream().listen((event) {
      if (event == MyCustomEventType.UserLoginIsComplete) {
        print('handling LOGIN COMPLETE event ' + event.toString());
      } else {
        print('handling some other event ' + event.toString());
      }
  }

  @override
  void dispose() {
    this.parentSubscription.cancel();
    super.dispose();
  }
}

请注意,如果您覆盖StreamController.done(),则您的侦听器将不会触发,因为done()将替换之前设置的任何侦听器。


注意:如果您的两个小部件之间存在一对一的通信关系,则不需要广播事件模式——在这种情况下,您可以创建没有.broadcast()的控制器,即使用StreamController<MyCustomEventType>(),而不是.stream.asBroadcastStream().listen(),您可以使用.stream.listen()进行侦听。有关详细信息,请参见https://api.dart.dev/stable/dart-async/Stream-class.html

有关此和其他方法的概述,请参见Inter Widget communication


5

我通过不断试错找到了另一种解决方案,而且它也有效。

import 'main.dart' as main;

然后在 onPressed 下面添加这一行。

main.MyAppState().printSample();

3
这是反模式,你应该将状态提升。 - buncis
2
在这里,您正在实例化一个新的状态对象,这是明显错误的。 - DarkNeuron
4
@ProCo 我的建议是删除这个回答。 - DarkNeuron
你能给出它的替代方案吗?这个答案很简单,但我不知道它会如何运作。 - Pro Co

0

好的,我想出了简单的解决方案

步骤1 -- 定义一个子小部件

    class MyChildWidget extends StatelessWidget {
      final Function detailsButtonFunction;
        MyChildWidget(
         {Key? key,
         required this.detailsButtonFunction,
       })
      : super(key: key);

   @override
     Widget build(BuildContext context) {
       return  Container(
          child: SingleChildScrollView(
            child: Container(
              child: ElevatedButton(
                  child: Text("Some Sample text")
                     onPressed : (){
                     detailsButtonFunction()
                   } 
                )
              ),
            ),
          ),
         );
        }
       }

步骤2- 将其放入父组件中

  class MyparentWidget extends StatelessWidget {

  MyParentWidget({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
     
        child: SingleChildScrollView(
          child: Container(
            child: Column(
              children:[ MyChildWidget(detailsButtonFunction:(){
                print("something")
                }  ),]
            ),
          ),
        ),
      ),
    );
  }
}

最终注意事项 -- 格式和拼写可能会有一些错误,请专注于函数部分... 当我们传递时,我们像这样做:(){Print("someThing")},当我们接收时,我们像这样做:(){functioname()}.... 添加括号非常重要。


您的回答可以通过添加更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便其他人可以确认您的答案是正确的。您可以在 帮助中心 中找到有关编写好答案的更多信息。 - mohammad mobasher

0

这里HomePage是父页面,ChildPage是子页面。有一个名为onSelectItem的方法,我们需要从子页面调用它。

class HomePage extends StatefulWidget {

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

class HomePageState extends State<HomePage> {

  onSelectItem(String param) {
    print(param);
  }

  @override Widget build(BuildContext context) {

  }
}

class ChildPage extends StatefulWidget {
  final HomePageState homePageState;

  ChildPage({Key key, @required this.homePageState}) : super(key: key);

  _ChildPageState createState() => _ChildPageState();
}

class _ChildPageState extends State<ChildPage> {
  @override Widget build(BuildContext context) {

    return RaisedButton(
      onPressed: () {
        widget.homePageState.onSelectItem("test");
      },
      child: const Text(
          'Click here',
          style: TextStyle(fontSize: 20)
      ),
    );
  }
}

所以,通过使用widget父类状态,我们可以调用父类方法。


0

如何从另一个类中调用方法或者使用StreamController从该类外部setState页面。

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

StreamController<int> streamController = StreamController<int>();

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

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

class MyHomePage extends StatefulWidget {
  MyHomePage(this.title, this.stream);
  final String title;
  final Stream<int> stream;

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

class _MyHomePageState extends State<MyHomePage> {
  String menuName = 'A';

  @override
  void initState() {
    super.initState();
    widget.stream.listen((index) {
      mySetState(index);
    });
  }

  void mySetState(int index) {
    List menuList = ['A', 'B', 'C'];
    setState(() {
      menuName = menuList[index];
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(30.0),
              child: Text(
                'Today\'s special is:',
                style: TextStyle(fontSize: 20),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(10.0),
              child: Text(
                'Combo ' + menuName,
                style: TextStyle(fontSize: 30, color: Colors.blue),
              ),
            ),
            SizedBox(
              height: 50,
            ),
            ElevatedButton(
              style: ButtonStyle(
                backgroundColor:
                    MaterialStateProperty.all<Color>(Colors.blue[700]),
              ),
              child: Text(
                'Settings',
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => SecondPage()),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

class SecondPage extends StatefulWidget {
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  List<bool> isSelected = [true, false, false];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Page'),
      ),
      body: Center(
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(30.0),
              child: Text(
                'Select a menu name:',
                style: TextStyle(fontSize: 20),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(20.0),
              child: ToggleButtons(
                children: <Widget>[
                  Text('A'),
                  Text('B'),
                  Text('C'),
                ],
                onPressed: (int index) {
                  setState(() {
                    for (int buttonIndex = 0;
                        buttonIndex < isSelected.length;
                        buttonIndex++) {
                      if (buttonIndex == index) {
                        isSelected[buttonIndex] = true;
                      } else {
                        isSelected[buttonIndex] = false;
                      }
                    }
                  });
                  streamController.add(index);
                },
                isSelected: isSelected,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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