有没有办法在Flutter中使组合小部件继承它所组成的小部件的属性?

3
我创建了一个小部件,根据其FocusNode的焦点,可以变成一个TextFieldText。它工作得很完美,这里是代码(由于太长,没有在此处包含)。
问题是,TextTextField有很多参数可用于设置它们的样式,我认为将所有这些参数复制到我的新混合小部件的构造函数中,仅仅是为了在新的build方法中将它们传递给这两个小部件而不做任何其他处理,不够优化。
例如,TextField在其构造函数中有超过50个参数,如果要使用另一个小部件组合它并仍然获得所有这些选项以设置TextField的样式,唯一的方法是将每个参数都复制到我的构造函数中,然后除了将它们传递给TextField之外不做任何其他操作?
那么,是否有某种设计模式或解决方案,使这两个小部件的参数在新小部件的构造函数中可用?
注:更多上下文,请参见此处的 M. Azyoksul在Gunter的评论中的评论
问题的最小化示例:
// this widget is from external library (not under my control)
class WidgetA extends StatelessWidget {
  // very long list of fields
     A1 a1;
     
     A2 a2;
     
     ... (long list of fields)

   // constructor
   WidgetA(this.a1, this.a2, ...);
  
}

// this widget is from external library
class WidgetB extends StatelessWidget {
  // very long list of fields
     B1 b1;
     
     B2 b2;
     
     ... (long list of fields)

   // constructor
   WidgetB(this.b1, this.b2, ...);
  
}


// now this is the widget I want to create
class HybridWidget extends StatelessWidget {

     // time consuming: I copy all the fields of WidgetA and 
     // WidgetB into the new constructor just to pass them as they are without doing anything else useful on them
     A1 a1;
     A2 a2;
     ...
     

     B1 b1;
     B2 b2;
     ...

 // the new constructor: (not optimal at all)
 HybridWidget(this.a1,this.a2,...,this.b1,this.b2,...);

  @override
  Widget build(BuildContext context) {
    // for example:
    if(some condition)
     return Container(child:WidgetA(a1,a2, ...),...); <--- here is the problem, I am not doing anything other than passing the "styling" parameters as they were passed to me, alot of copy/paste
    if(other condition)
      return Container(Widget2(b1,b2, ... ),...); <--- and here is the same problem
    
    //... other code
  }
}
3个回答

2

建造者模式可能适用(不确定这是否是正确的术语)。

首先定义我们函数的标识:

typedef TextBuilder = Widget Function(String text);
typedef TextFieldBuilder = Widget Function(TextEditingController, FocusNode);

这些将用于你的DoubleStatetext...
DoubleStateText(
    initialText: 'Initial Text',
    textBuilder: (text) => Text(text, style: TextStyle(fontSize: 18)),
    textFieldBuilder: (controller, focusNode) =>
        TextField(controller: controller, focusNode: focusNode, cursorColor: Colors.green,)
),

因此,我们不再将所有参数传递给DoubleStateText,而是传递包装TextTextField的所有所需参数的构建器(函数)。然后,DoubleStateText只调用构建器,而不是创建Text/TextField本身。

DoubleStateText的更改:

class DoubleStateText extends StatefulWidget {
  final String Function()? onGainFocus;

  final String? Function(String value)? onLoseFocus;

  final String initialText;

  // NEW ==================================================
  final TextBuilder textBuilder;

  // NEW ==================================================
  final TextFieldBuilder textFieldBuilder;

  final ThemeData? theme;

  final InputDecoration? inputDecoration;

  final int? maxLines;

  final Color? cursorColor;

  final EdgeInsets padding;

  final TextStyle? textStyle;

  const DoubleStateText({
    Key? key,
    this.onGainFocus,
    this.onLoseFocus,
    required this.initialText,
    required this.textBuilder, // NEW ==================================================
    required this.textFieldBuilder, // NEW ==================================================
    this.theme,
    this.inputDecoration,
    this.maxLines,
    this.cursorColor,
    this.padding = EdgeInsets.zero,
    this.textStyle,
  }) : super(key: key);

  @override
  State<DoubleStateText> createState() => _DoubleStateTextState();
}

class _DoubleStateTextState extends State<DoubleStateText> {
  bool _isEditing = false;
  late final TextEditingController _textController;
  late final FocusNode _focusNode;
  late final void Function() _onChangeFocus;

  @override
  void initState() {
    super.initState();

    _textController = TextEditingController(text: widget.initialText);
    _focusNode = FocusNode();

    // handle Enter key event when the TextField is focused
    _focusNode.onKeyEvent = (node, event) {
      if (event.logicalKey == LogicalKeyboardKey.enter) {
        setState(() {
          String? text = widget.onLoseFocus?.call(_textController.text);
          _textController.text = text ?? widget.initialText;
          _isEditing = false;
        });
        return KeyEventResult.handled;
      }
      return KeyEventResult.ignored;
    };

    // handle TextField lose focus event due to other reasons
    _onChangeFocus = () {
      if (_focusNode.hasFocus) {
        String? text = widget.onGainFocus?.call();
        _textController.text = text ?? widget.initialText;
      }
      if (!_focusNode.hasFocus) {
        setState(() {
          String? text = widget.onLoseFocus?.call(_textController.text);
          _textController.text = text ?? widget.initialText;
          _isEditing = false;
        });
      }
    };
    _focusNode.addListener(_onChangeFocus);
  }

  @override
  void dispose() {
    _textController.dispose();
    _focusNode.removeListener(_onChangeFocus);
    _focusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    Widget child;
    if (!_isEditing) {
      child = InkWell(
          onTap: () {
            setState(() {
              _isEditing = true;
              _focusNode.requestFocus();
            });
          },
          //child: Text(_textController.text, style: widget.textStyle),
          // NEW: use the builders ==========================================
          child: widget.textBuilder(_textController.text));
    } else {
      // NEW: use the builders ==========================================
      child = widget.textFieldBuilder(_textController, _focusNode);
      /*child = TextField(
        focusNode: _focusNode,
        controller: _textController,
        decoration: widget.inputDecoration,
        maxLines: widget.maxLines,
        cursorColor: widget.cursorColor,
      );*/
    }

    child = Padding(
      padding: widget.padding,
      child: child,
    );

    child = Theme(
      data: widget.theme ?? Theme.of(context),
      child: child,
    );

    return child;
  }
}


以下是如何使用上述内容的示例:
typedef TextBuilder = Widget Function(String text);
typedef TextFieldBuilder = Widget Function(TextEditingController, FocusNode);


class CompositeWidgetContent extends StatefulWidget {
  @override
  State<CompositeWidgetContent> createState() => _CompositeWidgetContentState();
}

class _CompositeWidgetContentState extends State<CompositeWidgetContent> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            SomeOtherFocusable(),
            SizedBox(
              height: 20,
            ),
            DoubleStateText(
                initialText: 'Initial Text',
                textBuilder: (text) =>
                    Text(text, style: TextStyle(fontSize: 18)),
                textFieldBuilder: (controller, focusNode) => TextField(
                      controller: controller,
                      focusNode: focusNode,
                      cursorColor: Colors.green,
                    )),
          ],
        ),
      ),
    );
  }
}

请注意,在_CompositeWidgetContentState中没有定义(text)(controller, focusNode)。它们不是由最终用户/客户端创建或使用的。这些是在DoubleStateText中创建的。

此外,这会破坏封装性。不小心使用 DoubleStateText 的用户现在可以持有对 _focusNodetextController 的引用,并且如果他们操作这些对象,则可能会破坏 DoubleStateText 的功能。 - HII
我认为在阅读那些构建器typedefs时存在误解。客户端/用户不传递textControllerfocusNode参数。这些由您的小部件DoubleStateText填写/使用。最终用户实际上是传递了一个函数的“签名”,而不是它们的“值”。(text) =>(ctrl,node) =>是所需的签名。然后,这些签名参数由您的DoubleStateText小部件使用。(有点令人费解。) - Baker
TextEditingController? outsideController; Widget w = DoubleStateText( textFieldBuilder: (ctrl, node) { outsideController = ctrl; //... }, ); }``` @Baker 这就是我的意思。 - HII
1
是的,我明白你的意思。我想他们可能会这样做(如果他们故意试图对使用“DoubleStateText”表示敌意,那么这是一个潜在的用例吗?)。无论如何,为了避免重复“Text”和“TextField”小部件构造函数参数的签名,我认为生成器模式是最简洁的方法。如果您找到更好的方法来实现此目标,请更新您的原始问题。我很乐意学习它。 - Baker
就像我告诉你的那样,这是一个非常好的方法。如果你找到了解决这个小问题的方法(当然是合理的),请在这里发布更新,将不胜感激。 - HII
显示剩余5条评论

0

我不是一个“安卓”专家,但在我看来,这些原则可以应用于这里:

  1. 为参数创建类

  2. 使用单例模式在组件之间共享数据

  3. 如果您想通知其他用户数据的更改,则可以使用观察者模式

让我解释一下我的意思。

1. 为参数创建类

您可以为参数创建类:

public class MyParams
{ 
    public int Param_1 { get; set; }

    public int Param_2 { get; set; }

    public int Param_3 { get; set; }
}

并使用它们:

class Widget1 extends StatelessWidget {
  // very long parameter list
   Widget1(MyParams params)
  
}

2. 使用单例模式在组件间共享数据

让我通过C#展示单例模式:

public sealed class MyParams
{
    public int Param_1 { get; set; }

    public int Param_2 { get; set; }

    public int Param_3 { get; set; }

    //the volatile keyword ensures that the instantiation is complete 
    //before it can be accessed further helping with thread safety.
    private static volatile MyParams _instance;
    private static readonly object SyncLock = new();

    private MyParams()  {}

    //uses a pattern known as double check locking
    public static MyParams Instance
    {
        get
        {
            if (_instance != null)
            {
                return _instance;
            }
            lock (SyncLock)
            {
                if (_instance == null)
                {
                    _instance = new MyParams();
                }
            }
            return _instance;
        }
    } 
}

然后您就可以在小部件中使用它:

class HybridWidget extends StatelessWidget {
  
  // how to get parameters of widget1 and widget2 here?
  // is there a way other than copying all of them?
  public void GetParams()
  {
      var params = MyParams.Instance;
  }
  
  // ... other code is omitted for the brevity      
 
}

React中的Redux工具广泛使用了Singleton和Observer等模式。 阅读这篇优秀文章以获取更多信息

更新

如果您有许多小部件,则可以将所有参数添加到参数集合WidgetParams中。 然后,您可以从任何其他小部件访问这些参数。

public sealed class MyParams
{
    //the volatile keyword ensures that the instantiation is complete 
    //before it can be accessed further helping with thread safety.
    private static volatile MyParams _instance;
    private static readonly object SyncLock = new();

    private MyParams()
    {
    }

    //uses a pattern known as double check locking
    public static MyParams Instance
    {
        get
        {
            if (_instance != null)
            {
                return _instance;
            }
            lock (SyncLock)
            {
                if (_instance == null)
                {
                    _instance = new MyParams();
                }
            }
            return _instance;
        }
    }

    List<WidgetParams> WidgetParams = new List<WidgetParams>();
}

public class WidgetParams
{
    /// <summary>
    /// Widget name
    /// </summary>
    public int Name { get; set; }

    public object Params { get; set; }
}

你可以从任何其他的小部件访问这些参数:

var params = MyParams.Instance.WidgetParams;

所以你组件中的代码看起来会像这样:

class HybridWidget extends StatelessWidget {
  
  // how to get parameters of widget1 and widget2 here?
  // is there a way other than copying all of them?
  public void GetParams()
  {
      var params = MyParams.Instance.WidgetParams;
  }
  
  // ... other code is omitted for the brevity      
 
}

当你说“你可以为参数创建类:...”和“并使用它们:...”时,不幸的是,“Widget1”和“Widget2”不在我的控制范围内,它们是随Flutter SDK一起提供的“material”库的一部分。因此,如果我按照您的建议去做,我将不得不将所有参数复制到这个类“MyParams”中,然后我可以在多个新小部件中使用它,但我正在寻找一种不必首先复制它们的方法。至于您提到的其他模式(2和3),我知道它们是什么,但没有完全理解您在这里的意思。谢谢。 - HII
因为如果我将参数复制到这个新类MyParams中,那么每次我需要创建一个由多个其他小部件组成的小部件(在Flutter中我们确实经常这样做),那么我需要每次将每个小部件的参数复制到一个新的类ThatWidgetParams中,然后才能在新的组合小部件中使用它。 - HII
请提供需要翻译的英语文本。 - HII
@Haidar,请查看我的更新答案。 - StepUp
1
@haidar 可能吧。好的,让我们等待其他回复。 - StepUp
显示剩余5条评论

0
我处理这个问题的方式是将TextTextfield作为参数传递,而不是询问所有参数。
const DoubleStateText({
    Key? key,
    required this.initialText,
    required this.textFieldWidget,
    required this.textWidget,
    this.onGainFocus,
    this.onLoseFocus,
    this.padding = EdgeInsets.zero,
  }) : super(key: key);

这样我可以保持简单: DoubleStateText 的作用仅仅是智能切换小部件。


但是我该如何在TextField上使用TextEditingControllerFocusNode呢?(你知道小部件是不可变的)如果你的意思是我应该在DoubleStateTextbuild中再次创建TextTextField,那么我们又回到了同样的问题,但现在它不是在构造函数的级别上,而是在build方法的级别上。但是,如果我只是像你说的那样进行“切换”,那么这当然比我的方法更好,但不幸的是,在这里我不是这样做的。 - HII

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