Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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
  }
}
like image 882
Haidar Avatar asked Dec 07 '25 07:12

Haidar


1 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.

like image 190
Baker Avatar answered Dec 08 '25 19:12

Baker