[Working solution included] Observable TextField
Currently, as is seen in the example, TextField's text is not observable.
My naive attempt is as follows. Not sure whether this helps!
import 'package:flutter/material.dart';
import 'package:lombok_annotation/lombok_annotation.dart';
import 'package:mobx/mobx.dart';
class SyncTextField extends StatefulWidget {
final GetSet<String> gs;
// forward
final InputDecoration? decoration;
final TextInputType? keyboardType;
const SyncTextField({Key? key, required this.gs, this.decoration, this.keyboardType}) : super(key: key);
@override
_SyncTextFieldState createState() => _SyncTextFieldState();
}
class _SyncTextFieldState extends State<SyncTextField> {
late TextEditingController _controller;
late VoidCallback _disposer;
@override
void initState() {
super.initState();
_controller = TextEditingController();
_disposer = reaction<String>(
(_) => widget.gs.getter(),
(val) => _controller.value = _controller.value.copyWith(text: val),
fireImmediately: true,
);
}
@override
void dispose() {
_disposer();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
onChanged: widget.gs.setter,
// forward
decoration: widget.decoration,
keyboardType: widget.keyboardType,
);
}
}
This is quite helpful, because in the MobX world, it would be great if everything is observable, and TextField is very frequently used :)
@pavanpodila If the prototype looks good I will PR for it.
I think these kind of things should be kept in a separate package: mobx_extensions. We should do that and then over time based on maturity we can bring them into core
Thanks for the reply. I will create that package later when having time.
So, maybe the documentation of mobx can mention that extensions package? Since these are quite useful things.
This would be very useful in a package. Along with other Observable form fields.
Was this ever made? I can't find mobx_extensions
No it was not, but I have been using it for a long time. Below is the latest code copy-pasted from my internal library, which is fairly simple.
P.S. In addition to SyncTextField, there is SyncIntTextField which handles integer input.
import 'dart:math';
import 'dart:ui' as ui;
import 'package:convenient_test_common/src/common_flutter/get_set.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobx/mobx.dart';
class SyncTextField extends StatefulWidget {
final GetSet<String> gs;
// forward
final FocusNode? focusNode;
final InputDecoration? decoration;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final TextCapitalization textCapitalization;
final TextStyle? style;
final StrutStyle? strutStyle;
final TextAlign textAlign;
final TextAlignVertical? textAlignVertical;
final TextDirection? textDirection;
final bool autofocus;
final String obscuringCharacter;
final bool obscureText;
final bool autocorrect;
final bool enableSuggestions;
final int? maxLines;
final int? minLines;
final bool expands;
final bool readOnly;
final bool? showCursor;
final int? maxLength;
final MaxLengthEnforcement? maxLengthEnforcement;
final ValueChanged<String>? onChanged;
final VoidCallback? onEditingComplete;
final ValueChanged<String>? onSubmitted;
final AppPrivateCommandCallback? onAppPrivateCommand;
final List<TextInputFormatter>? inputFormatters;
final bool? enabled;
final double cursorWidth;
final double? cursorHeight;
final Radius? cursorRadius;
final Color? cursorColor;
final ui.BoxHeightStyle selectionHeightStyle;
final ui.BoxWidthStyle selectionWidthStyle;
final Brightness? keyboardAppearance;
final EdgeInsets scrollPadding;
final bool enableInteractiveSelection;
final TextSelectionControls? selectionControls;
final DragStartBehavior dragStartBehavior;
final GestureTapCallback? onTap;
final MouseCursor? mouseCursor;
final InputCounterWidgetBuilder? buildCounter;
final ScrollPhysics? scrollPhysics;
final ScrollController? scrollController;
final Iterable<String>? autofillHints;
final Clip clipBehavior;
final String? restorationId;
final bool enableIMEPersonalizedLearning;
const SyncTextField({
super.key,
required this.gs,
// forward
this.focusNode,
this.decoration = const InputDecoration(),
this.keyboardType,
this.textInputAction,
this.textCapitalization = TextCapitalization.none,
this.style,
this.strutStyle,
this.textAlign = TextAlign.start,
this.textAlignVertical,
this.textDirection,
this.readOnly = false,
this.showCursor,
this.autofocus = false,
this.obscuringCharacter = '•',
this.obscureText = false,
this.autocorrect = true,
this.enableSuggestions = true,
this.maxLines = 1,
this.minLines,
this.expands = false,
this.maxLength,
this.maxLengthEnforcement,
this.onChanged,
this.onEditingComplete,
this.onSubmitted,
this.onAppPrivateCommand,
this.inputFormatters,
this.enabled,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorColor,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.mouseCursor,
this.buildCounter,
this.scrollController,
this.scrollPhysics,
this.autofillHints = const <String>[],
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.enableIMEPersonalizedLearning = true,
});
@override
_SyncTextFieldState createState() => _SyncTextFieldState();
}
class _SyncTextFieldState extends State<SyncTextField> {
late TextEditingController _controller;
late VoidCallback _disposer;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.gs.getter());
_disposer = reaction<String>((_) => widget.gs.getter(), _handleGsChange).call;
}
@override
void dispose() {
_disposer();
_controller.dispose();
super.dispose();
}
void _handleGsChange(String newText) {
int _clampOffset(int raw) => min(raw, newText.length);
final oldValue = _controller.value;
_controller.value = oldValue.copyWith(
text: newText,
selection: oldValue.selection.copyWith(
baseOffset: _clampOffset(oldValue.selection.baseOffset),
extentOffset: _clampOffset(oldValue.selection.extentOffset),
),
composing: TextRange(
start: _clampOffset(oldValue.composing.start),
end: _clampOffset(oldValue.composing.end),
),
);
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
onChanged: (v) {
widget.gs.setter(v);
widget.onChanged?.call(v);
},
// forward
focusNode: widget.focusNode,
decoration: widget.decoration,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
style: widget.style,
strutStyle: widget.strutStyle,
textAlign: widget.textAlign,
textAlignVertical: widget.textAlignVertical,
textDirection: widget.textDirection,
readOnly: widget.readOnly,
showCursor: widget.showCursor,
autofocus: widget.autofocus,
obscuringCharacter: widget.obscuringCharacter,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
enableSuggestions: widget.enableSuggestions,
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
maxLength: widget.maxLength,
maxLengthEnforcement: widget.maxLengthEnforcement,
onEditingComplete: widget.onEditingComplete,
onSubmitted: widget.onSubmitted,
onAppPrivateCommand: widget.onAppPrivateCommand,
inputFormatters: widget.inputFormatters,
enabled: widget.enabled,
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: widget.cursorRadius,
cursorColor: widget.cursorColor,
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
keyboardAppearance: widget.keyboardAppearance,
scrollPadding: widget.scrollPadding,
dragStartBehavior: widget.dragStartBehavior,
enableInteractiveSelection: widget.enableInteractiveSelection,
selectionControls: widget.selectionControls,
onTap: widget.onTap,
mouseCursor: widget.mouseCursor,
buildCounter: widget.buildCounter,
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
autofillHints: widget.autofillHints,
clipBehavior: widget.clipBehavior,
restorationId: widget.restorationId,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
);
}
}
class SyncIntTextField extends StatelessWidget {
final GetSet<int> gs;
// forward
final UiElementLid? uiId;
final InputDecoration? decoration;
// ignore: unnecessary-nullable
const SyncIntTextField({
super.key,
required this.gs,
required this.uiId,
this.decoration,
});
@override
Widget build(BuildContext context) {
return SyncTextField(
gs: _gsIntToGsString(gs),
keyboardType: TextInputType.number,
// forward
uiId: uiId,
decoration: decoration,
);
}
}
GetSet<String> _gsIntToGsString(GetSet<int> gsInt) => GetSet(
getter: () => gsInt.getter().toString(),
setter: (s) {
final i = int.tryParse(s);
if (i != null) gsInt.setter(i);
});
@fzyzcjy this seems super interesting, looks like this will allow me to send the mobx observable to the TextField right? How do you use the GetSet class? It looks like you use this class to wrap mobx observables and the send the GetSet to the TextField right?
Would you be so kind to show me how to use this using a Mobx Store?
Thanks
looks like this will allow me to send the mobx observable to the TextField right?
Yes, automatically synchronize textfield with observable.
GetSet
class GetSet<T> {
T Function() getter;
void Function(T value) setter;
GetSet({required this.getter, required this.setter});
GetSet.gs(this.getter, this.setter);
}
Usage:
class YourStore { @observable String a; }
SyncTextField(
gs: GetSet(getter: () => yourStore.a, setter: (value) => yourStore.a = value),
)
@fzyzcjy thank you Yo! I'm using it already in my app. works great!
Not sure exactly how it works but I'll figure it out.
You are welcome!