Is there a way to make a composited widget inherit the properties of the widgets it is composed from in flutter?

Viewed 149

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
  }
}
3 Answers

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.

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      
 
}

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.

Related