【问题标题】:Calling method from State class从 State 类调用方法
【发布时间】:2019-01-28 05:01:17
【问题描述】:

给定一个有状态的小部件,以某种方式可以调用在 State 类中定义的方法(extends State<NameOfTheWidget>)。实际上,我只是想重建 _State 类,就像调用 setState() 但从类外部。我知道如何从孩子到父母,但反之亦然。

class Foo extends StatefulWidget{
  State createState() => new _State();
  //...bar() ??
}

class _State extends State<Foo>{
  @override
  Widget build(BuildContext context) {...}

  void bar(){...}
}

编辑:一些真实的代码

首先,我们拥有内部小部件的等价物;这是一个自定义的文本字段。关键是我想根据布尔 _activo 变量启用和禁用它。

import 'package:flutter/material.dart';
import 'package:bukit/widgets/ensure.dart';

class EntradaDatos extends StatelessWidget{
  final String _titulo;
  final String _hint;
  TextEditingController _tec;
  FocusNode _fn = new FocusNode();
  final String Function(String s) _validador;
  final TextInputType _tit;
  bool _activo;

  /*
   *  CONSTRUCTOR
   */
  EntradaDatos(this._titulo, this._hint, this._validador, this._tit, this._activo){
    _tec = new TextEditingController();     
  }

  @override
  Widget build(BuildContext context){
    print('Construyendo');
    return new EnsureVisibleWhenFocused(
      focusNode: _fn,
      child: new TextFormField(
        enabled: _activo,
        keyboardType: _tit,
        validator: _validador,
        autovalidate: true,
        focusNode: _fn,
        controller: _tec,
        decoration: InputDecoration(
          labelText: _titulo,
          hintText: _hint
        ),
      )
    );
  }

  String getContenido(){
    return _tec.text;
  }
}

然后我有一个前一个文本字段的具体实现,它只是扩展它:

import 'package:flutter/material.dart';
import 'package:bukit/widgets/entrada_datos.dart';

class EntradaMail extends EntradaDatos{

  static String _hint = "nombre@dominio.es";
  static String _validador(String s){
    if(s.isEmpty){
      return 'El campo es obligatorio';
    }else{
      if(!s.contains('@') || !s.contains('.') || s.contains(' ')){
        return 'Introduce una dirección válida';
      }else{
        String nombre = s.substring(0, s.indexOf('@'));
        String servidor = s.substring(s.indexOf('@')+1, s.lastIndexOf('.'));
        String dominio = s.substring(s.lastIndexOf('.')+1);
        if(nombre.length < 2 || servidor.length < 2 || dominio.length < 2){
          return 'Introduce una dirección válida';
        }
      }
    }
  }

  EntradaMail(String titulo, bool activo) : super(titulo, _hint, _validador, TextInputType.emailAddress, activo);
}

最后,相当于我的外部小部件。它只是一个复选框,后跟 prevoius EntradaEmail 小部件。据我所知,一旦按下复选框并进行 onChange 调用,setState 调用应该重建所有内容,但我与调试消息形成对比,即永远不会调用第一个内部小部件的 build 方法。我的意思是根据复选框启用和禁用文本字段。

class CampoEnvio extends StatefulWidget{

  EntradaMail _mail;
  EntradaMovil _movil;
  String _tituloMail;
  String _tituloMovil;  
  bool _usaMail = false;
  bool _usaMovil = false;

  CampoEnvio(this._tituloMail, this._tituloMovil){
    _mail = new EntradaMail(_tituloMail, _usaMail);
    _movil = new EntradaMovil(_tituloMovil, _usaMovil);
  }

  State createState() => _State(_mail, _movil, _usaMail, _usaMovil, _tituloMail, _tituloMovil);

}

class _State extends State<CampoEnvio>{

  bool _usaMail;
  bool _usaMovil;
  String _tituloMail;
  String _tituloMovil;
  EntradaMail _mail;
  EntradaMovil _movil;

  _State(this._mail, this._movil, this._usaMail, this._usaMovil, this._tituloMail, this._tituloMovil);


  @override
  Widget build(BuildContext context){
    return new Column(
      children: <Widget>[
        new ListTile(
          leading: new SizedBox(
            width: 70.0,
            child: new Row(
              children: <Widget>[
                new Checkbox(
                  value: _usaMail,
                  activeColor: Colors.black,
                  onChanged: (value) {
                    setState(() {
                      _usaMail = value;                 
                    });
                  },
                ),
              ],
            ),
          ),
          title: _mail,
        ),
        //...
        new Divider()
      ],
    );
  }
}

【问题讨论】:

  • 你能不能详细点?也许你能说出你想打电话的原因/时间bar()

标签: flutter


【解决方案1】:

是的,理论上可以使用GlobalKey但不推荐!

class OuterWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => OuterWidgetState();
}

class OuterWidgetState extends State<OuterWidget> {
  final _innerKey = GlobalKey<InnerWidgetState>();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        InnerWidget(key: _innerKey),
        RaisedButton(
          child: Text('call foo'),
          onPressed: () {
            _innerKey.currentState.foo();
          },
        )
      ],
    );
  }
}

class InnerWidget extends StatefulWidget {
  InnerWidget({Key key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => InnerWidgetState();
}

class InnerWidgetState extends State<InnerWidget> {
  String _value = 'not foo';

  @override
  Widget build(BuildContext context) {
    return Text(_value);
  }

  void foo() {
    setState(() {
      _value = 'totally foo';
    });
  }
}

更好的方法:相反,提升状态是个好主意:

class OuterWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => OuterWidgetState();
}

class OuterWidgetState extends State<OuterWidget> {
  String _innerValue;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        InnerWidget(value: _innerValue),
        RaisedButton(
          child: Text('call foo'),
          onPressed: () {
            setState(() {
              _innerValue = 'totally foo';
            });
          },
        )
      ],
    );
  }
}

class InnerWidget extends StatefulWidget {
  InnerWidget({Key key, this.value}) : super(key: key);

  final String value;

  @override
  State<StatefulWidget> createState() => InnerWidgetState();
}

class InnerWidgetState extends State<InnerWidget> {
  @override
  Widget build(BuildContext context) {
    return Text(widget.value);
  }
}

如果可以,让内部小部件无状态:

class InnerWidget extends StatelessWidget {
  InnerWidget({Key key, this.value}) : super(key: key);

  final String value;

  @override
  Widget build(BuildContext context) {
    return Text(value);
  }
}

如果您的孩子是交互式的(点击、复选框...),您可以使用VoidCallbackValueChanged&lt;T&gt;(或您自己的typedef)定义回调来处理父窗口小部件中的事件。

【讨论】:

  • 感谢您的回答@boformer,您的第二个方法正是我正在做的事情。但是,一旦我在外部小部件中调用 setState() ,内部小部件就不会重建。我能看到的唯一区别是我没有在内部小部件的构造函数中使用键。
  • 这很奇怪。当您在外部小部件上调用 setState 时,内部小部件应始终重建。总是,即使在构造函数中没有key。你能分享你的确切代码吗?另外,如果我的回答对您有帮助,请将其标记为已接受。
  • 看我的第二个回答
【解决方案2】:

好的,既然您添加了示例代码,我将尝试解释为什么您的小部件无法工作,并且我将尝试解释可以进行哪些其他改进。


首先,您可以通过为所有小部件使用命名构造函数来提高代码的可读性,就像在我的其他答案中一样(您可以使用 Android Studio 自动生成它们:定义一些最终字段,然后按下灯泡按钮生成构造函数)。


下一个问题是创建TextEditingController 的小部件必须始终是有状态的小部件!否则每次构建后用户输入的内容都会消失!

通常您会从父小部件(在您提交数据时处理数据的小部件)传入TextEditingController


此外,不鼓励扩展小部件。相反,使用组合,例如:

class EntradaMail extends StatelessWidget {
  final String titulo;
  // ...    
  Widget build(BuildContext context) {
    return EntradaDatos(
      titulo: titulo,
      //...
    )
  }
}

小部件属性应该始终是公共的和最终的(永远不要以 _ 开头)。


你在CampoEnvio做一些奇怪的事情。

首先,您出于某种原因将小部件的所有属性传递给createState 中的State。这会产生一些你可能不想要的后果。

一般来说,您的State 类具有构造函数参数非常很少见,通常您会将属性从有状态小部件传递到状态。

问题是createState 只被调用一次,当您在父窗口小部件中调用initState 时不会再次调用它。状态会一直保持到小部件被释放为止。

这意味着您的状态构造函数也只被调用一次,_StateCampoEnvio)中的字段将始终保持不变。即使重新构建父级并再次调用CampoEnvio的构造函数,_State中的旧值也不会被替换。


StatefulWidget 中创建小部件(EntradaMailEntradaMovil)也很奇怪。

扩展StatefulWidget 的类不应该那样做!它基本上只是一个属性“包”。


这是完整的固定示例代码,遵循上述约定:

class EntradaDatos extends StatefulWidget {
  EntradaDatos({Key key, this.titulo, this.hint, this.validador, this.tit, this.activo}) : super(key: key);

  final String titulo;
  final String hint;

  final String Function(String s) validador;
  final TextInputType tit;
  final bool activo;

  @override
  State<StatefulWidget> createState() => _EntradaDatosState();
}

class _EntradaDatosState extends State<EntradaDatos> {
  // FocusNode and TextEditingController must be the same for the whole lifetime of the widget
  // => put into State
  TextEditingController _tec;
  FocusNode _fn;

  @override
  void initState() {
    super.initState();
    _tec = new TextEditingController();
    _fn = new FocusNode();
  }

  @override
  Widget build(BuildContext context) {
    print('Construyendo');
    return new EnsureVisibleWhenFocused(
        focusNode: _fn,
        child: new TextFormField(
          enabled: widget.activo,
          keyboardType: widget.tit,
          validator: widget.validador,
          autovalidate: true,
          focusNode: _fn,
          controller: _tec,
          decoration: InputDecoration(labelText: widget.titulo, hintText: widget.hint),
        ));
  }

  String getContenido() {
    return _tec.text;
  }
}

class EntradaMail extends StatelessWidget {
  static String _hint = "nombre@dominio.es";

  static String _validador(String s) {
    if (s.isEmpty) {
      return 'El campo es obligatorio';
    } else {
      if (!s.contains('@') || !s.contains('.') || s.contains(' ')) {
        return 'Introduce una dirección válida';
      } else {
        String nombre = s.substring(0, s.indexOf('@'));
        String servidor = s.substring(s.indexOf('@') + 1, s.lastIndexOf('.'));
        String dominio = s.substring(s.lastIndexOf('.') + 1);
        if (nombre.length < 2 || servidor.length < 2 || dominio.length < 2) {
          return 'Introduce una dirección válida';
        }
      }
    }
  }

  EntradaMail({Key key, this.titulo, this.activo}) : super(key: key);

  final String titulo;
  final bool activo;

  @override
  Widget build(BuildContext context) {
    // use composition instead of inheritance
    return EntradaDatos(
      titulo: titulo,
      activo: activo,
      validador: _validador,
      hint: _hint,
      tit: TextInputType.emailAddress,
    );
  }
}

class CampoEnvio extends StatefulWidget {
  const CampoEnvio({Key key, this.tituloMail, this.tituloMovil}) : super(key: key);

  final String tituloMail;
  final String tituloMovil;

  @override
  State<StatefulWidget> createState() => new _CampoEnvioState();
}

class _CampoEnvioState extends State<CampoEnvio> {
  // I guess these variables are modified here using setState
  bool _usaMail;
  bool _usaMovil;

  @override
  Widget build(BuildContext context) {
    // just rebuild the widgets whenever build is called!
    final mail = new EntradaMail(
      titulo: widget.tituloMail,
      activo: _usaMail,
    );
    final movil = new EntradaMovil(
      titulo: widget.tituloMovil,
      activo: _usaMovil,
    );

    return new Column(
      children: <Widget>[
        new ListTile(
          leading: new SizedBox(
            width: 70.0,
            child: new Row(
              children: <Widget>[
                new Checkbox(
                  value: _usaMail,
                  activeColor: Colors.black,
                  onChanged: (value) {
                    setState(() {
                      _usaMail = value;
                    });
                  },
                ),
              ],
            ),
          ),
          title: mail,
        ),
        //...
        new Divider()
      ],
    );
  }
}

查看 Flutter 存储库中的官方示例总是有帮助的!

【讨论】:

  • 非常感谢您的解释性回答,最后一个问题:有没有办法从外部小部件访问 TextEditingController,或者应在 _CampoEnvioState 中声明它并将其作为构造函数的参数传递(等等与 EntradaMail)?再次感谢。
  • 您将在外部小部件中声明 TextEditingController(必须是有状态的)并将其保存在 State 的变量中,然后使用构造函数参数传递控制器(例如,最终的 @987654342 EntradaDatos 类和 EntradaMail 类中的 @ 字段。)。在_EntradaDatosState 类中,您将通过widget.controller 访问控制器
  • 首先,您可能会将相同的值保存在 2 个位置(子小部件中的控制器,以及一些在调用函数时也会保存该值的父小部件),这绝不是一个好方法想法。
  • 控制器总是让你访问当前值,而函数只报告变化。
  • AnimationControllerScrollControllerPageController 相同:这些控制器允许您操纵孩子的状态,它们还允许您访问孩子的当前状态
猜你喜欢
  • 1970-01-01
  • 2023-03-04
  • 2017-11-09
  • 2011-10-24
  • 1970-01-01
  • 1970-01-01
  • 2011-01-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多