3

I created a widget that, depending on the focus of its FocusNode, either becomes a TextField or a Text. It is working perfectly and here is the code (didn't include it here as its large).

The problem is, Text and TextField have really alot of parameters to style them, and I find it not optimal to copy all these parameters into the constructor of my new hybrid widget just to pass them to these two widgets in the new build method without doing anything else with them.

For example TextField has over 50 parameters in its constructor, is the only way to compose it with another widget and still get all these options to style the TextField, is by copying each one of these parameters into my constructor, and then not doing anything with them other than passing them down to the TextField?

So is there some design pattern or some solution that lets the parameters of these 2 widgets be available in the constructor of the new widget?

note: see the comment of M. Azyoksul on Gunter's comment here also for more context.

minimal example of the problem:

// 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
  }
}
HII
  • 3,420
  • 1
  • 14
  • 35

3 Answers3

2

Builder pattern might work (not sure if that's the right term).

First define our Function signatures:

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

Those will be used in your DoubleStatetext...

DoubleStateText(
    initialText: 'Initial Text',
    textBuilder: (text) => Text(text, style: TextStyle(fontSize: 18)),
    textFieldBuilder: (controller, focusNode) =>
        TextField(controller: controller, focusNode: focusNode, cursorColor: Colors.green,)
),

... so instead of passing all the args to DoubleStateText we pass it builders (functions) that wrap the Text and TextField with all the args we want. Then DoubleStateText just calls the builders instead of creating the Text/TextField itself.

Changes to 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;
  }
}

Here's an example of how the above would be used:

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,
                    )),
          ],
        ),
      ),
    );
  }
}

Notice that the (text) and (controller, focusNode) are not defined anywhere in _CompositeWidgetContentState.

Those are not created/used by the end-user/client.

Those are created within DoubleStateText.

Baker
  • 24,730
  • 11
  • 100
  • 106
  • nice approach, almost solves the problem, but one small problem, you are passing the `_focusNode` and the `_textController` to the user, so when I call the builders in the `build` method, the user may modify the `_textController` and `_focusNode`, which is not intended as these fields should not be visible to the user of the `DoubleStateText`, I just want him to pass me the styling parameters, can you fix it and I mark it as accepted? (same applies to the `Text` widget builder, I only need the styling params, so forcing user to pass `text` is useless) – HII Aug 19 '22 at 14:43
  • also this breaks encapsulation, non-careful users of `DoubleStateText` now can hold on to reference to `_focusNode` or `textController` and break the functionality of the `DoubleStateText` if they play with these objects. – HII Aug 19 '22 at 14:49
  • I think there's a misunderstanding in reading those builder `typedefs`. The client/user doesn't pass the `textController` nor `focusNode` args. Those are filled-in/used by your widget `DoubleStateText`. The end-user is in effect, passing a "signature" of a function, but not their "values". The `(text) => ` & `(ctrl, node) =>` is the signature that's required. Those signature args are then used by your `DoubleStateText` widget. (It's a bit of a mind-bender.) – Baker Aug 19 '22 at 14:59
  • I'll add an example usage to help illustrate – Baker Aug 19 '22 at 15:03
  • ```void main() { TextEditingController? outsideController; Widget w = DoubleStateText( textFieldBuilder: (ctrl, node) { outsideController = ctrl; //... }, ); }``` @Baker this is what I mean – HII Aug 19 '22 at 15:07
  • yes in your example you are depending on the user to return, in the builders, the same `text`, `controller`, and `focusNode` that were passed to him, and you are supposing he will not return some other controllers that he holds reference to, and which he can change later from outside, so you are giving him oppurtunity to break the `DoubleStateText` (see my comment above, for example: `// now user can break the functionality of the widget like this: outsideController.text = "123";`). Also, he may return other text than `text`, and it will have no effect as we are using `initialText` instead – HII Aug 19 '22 at 15:13
  • I think I understand what you're saying. You're wanting a signature where the end user can provide all the styling, but has no *visibility* to the `text`, `controller, node`. In other words, you're looking to hide those details from the end-user as they may confuse them about how the Builder pattern / function signatures work and may inadvertantly create and *try* to pass a controller (which, would actually never be used, since they are replaced within `DoubleStateText`). Am I understanding correctly? – Baker Aug 19 '22 at 15:31
  • here: `child = widget.textFieldBuilder(_textController, _focusNode);` you are calling the method returned to you from the user, who may have returned to you this method when he created the `DoubleStateText`: ```textFieldBuilder: (ctrl, node) { someControllerFromOutside = ctrl; //... },``` and later in the code the user can execute: `someControllerFromOutside.text = "123";` So as you can see, he can capture the value of the controller and modify it later, breaking the functionality of the widget – HII Aug 19 '22 at 15:36
  • 1
    Yup, I see what you're saying. I suppose they could do that (if they are purposely trying to be adversarial about using `DoubleStateText`. Is that a potential use case?). Anyways, to avoid having to duplicate the signature of `Text` and `TextField` widget constructor arguments I think the Builder pattern is the leanest way to it. Please update your original question if you find a better way to this goal. I'd be keen to learn it. – Baker Aug 19 '22 at 15:52
  • as i told you, very nice approach, if you find a way to solve this little problem (which is legitimate of course), it would be much appreciated to post here an update – HII Aug 19 '22 at 15:56
0

I am not android guy, however, in my view, these principles can be applied here:

  1. make class for parameters

  2. use singleton pattern to share data between your components

  3. if you want to notify other users about changing of data, then you can use observer pattern

Let me clarify what I mean.

1. Make class for parameters

You can create class for parameters:

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

    public int Param_2 { get; set; }

    public int Param_3 { get; set; }
}

and use them:

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

2. Use singleton pattern to share data between your components

Let me show singleton pattern via 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;
        }
    } 
}

and then you can use it in your widgets:

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      
 
}

Redux tool in React extensively uses these patterns such as Singleton and Observer. Read more in this great article.

UPDATE

If you have many widgets, then you can add all params in params collection WidgetParams. And then you can access to these parameters from any other widgets.

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; }
}

You can access to these parameters from any other widgets:

var params = MyParams.Instance.WidgetParams;

So code in your component would look like this:

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      
 
}
StepUp
  • 36,391
  • 15
  • 88
  • 148
  • When you say `You can create class for parameters: ...` and `and use them: ...`, then unfortunately, `Widget1` and `Widget2` are not under my control, they are part of the `material` library that comes with the Flutter SDK. So if I was to do what you are suggesting I would have to copy all the parameters into this class `MyParams`, and ok I can use it after that in multiple new widgets, but I am looking for a way to not copy them in the first place. As for the other patterns you mentioned (2 and 3), I know what they are but didn't catch exactly how you mean I should use them here. Thx. – HII Aug 01 '22 at 09:09
  • Because If I copy the parameters into this new class `MyParams`, then every time I need to create a widget that is composed of multiple other widgets (and we do this really alot in flutter) then I need to every time copy the params of each widget into a new class `ThatWidgetParams` and then I can use it in the new composed widget – HII Aug 01 '22 at 09:11
  • see the edited question for more clarification – HII Aug 01 '22 at 09:11
  • @Haidar please, see my updated answer – StepUp Aug 01 '22 at 09:40
  • sorry I think we are not on the same page, however I updated the question to make my intent very clear, what you are suggesting still makes me need to copy all the parameter list of the external widgets if I want to use them in my composited widget – HII Aug 01 '22 at 10:05
  • @Haidar do you mean that you want to find a design pattern which allows to avoid filling a lot of parameters in `Widget build(BuildContext context)` method? – StepUp Aug 01 '22 at 10:38
  • `to avoid filling a lot of parameters in`, yes , but not in build, rather in the constructor of `HybridWidget` i.e. when I declare it: `HybridWidget(this.w1p1,...,this.w1p50,this.w2p1,...,this.w2p40);`, I don't want to recreate the list of parameters of each included widget, this is very time consuming – HII Aug 01 '22 at 10:45
  • @Haidar I think that there is no pattern that fill in params of methods. – StepUp Aug 01 '22 at 10:49
  • there must be a way in flutter, since in flutter we compose widgets,thus what i am doing above does not make sense at all,there must be a pattern to transfer params into the hybrid widget – HII Aug 01 '22 at 11:16
  • 1
    @haidar maybe. ok, let's wait for other replies – StepUp Aug 01 '22 at 11:17
0

The way I'd handle it would be to pass the Text and Textfield as params instead all the parameters you're asking.

const DoubleStateText({
    Key? key,
    required this.initialText,
    required this.textFieldWidget,
    required this.textWidget,
    this.onGainFocus,
    this.onLoseFocus,
    this.padding = EdgeInsets.zero,
  }) : super(key: key);

This way I'd keep things simple : the role of the DoubleStateText is only to do a smart switch of widgets.

FDuhen
  • 4,097
  • 1
  • 15
  • 26
  • but then how can I use a `TextEditingController` and `FocusNode` on the `TextField`, (you know widgets are immutable)? If you mean that I should -in the `build` of `DoubleStateText`- create `Text` and `TextField` again then we are back to the same problem but now its not on the level of the constructor but on the level of the `build` method. But of course this is better approach than mine if I was only doing "switching" as you said, but here i'm not unfortunately – HII Aug 16 '22 at 06:32