I've made a simple widget wrapper around TextFormField
to avoid using TextEditingController
everywhere and simplify working with text fields. Basically, it's bidirectional binding between model and view (changing model rebuilds widget, and changing text in the widget updates the model). It works ok, but if I use this component inside Form
updating model causes Form
widget to generate the well known "setState() or markNeedsBuild called during build" exception. Here is the simplified code:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => const MaterialApp(home: MyHomePage());
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _counter = ValueNotifier('Initial text');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter Demo')),
body: Padding(
padding: const EdgeInsets.all(15.0),
child: Form(
child: ListView(
children: [
ValueListenableBuilder(
valueListenable: _counter,
builder: (context, _, __) => TextInput(
value: _counter.value,
onChanged: (value) => _counter.value = value,
),
),
ValueListenableBuilder(
valueListenable: _counter,
builder: (context, _, __) => Text(_counter.value),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _counter.value += 'a',
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class TextInput extends StatefulWidget {
final String? value;
final ValueChanged<String>? onChanged;
const TextInput({
this.value,
this.onChanged,
super.key,
});
@override
State<TextInput> createState() => _TextInputState();
}
class _TextInputState extends State<TextInput> {
late final _controller = TextEditingController(text: widget.value);
@override
void didUpdateWidget(TextInput oldWidget) {
super.didUpdateWidget(oldWidget);
final value = widget.value ?? '';
if (_controller.text != value) {
_controller.text = value;
}
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
void _onChanged(String value) {
try {
widget.onChanged!.call(value);
} catch (e) {
if (kDebugMode) {
rethrow;
}
}
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: _controller,
onFieldSubmitted: widget.onChanged != null ? _onChanged : null,
);
}
}
Is it an expected behavior? If so, what is the right fix? Or maybe it's a bug? As far as I understand, rebuilding TextInput
rebuilds parent Form
and the exception is thrown. If it's an expected behavior, how should I implement TextInput
widget? How should it update its state on model change outside without generating Form
exceptions?