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
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With