Flutter: Update TextFormField text with ChangeNotifier

2.1k Views Asked by At

In a complex scenario I need to update the text of some TextFormFields when a notifyListeners() is sent by a Model extending ChangeNotifier. The problem is that to change the text of a TextFormField you have to use the setter TextFormField.text which implies a rebuild, and so you can't use it into the build method. But to access the Provider of the model you need the context which is inside the build method.

MWE (obviously the button is in another Widget in the real project, and there are more TextFormFields)

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyModel extends ChangeNotifier {
  void updateCounter() {
    ++_counter;
    notifyListeners();
  }

  MyModel() {
    _counter = 1;
  }

  int _counter;
  String get counter => _counter.toString();
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyModel(),
      child: MaterialApp(
        title: 'Test',
        home: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var _text1Ctl = TextEditingController();
  var _text2Ctl = TextEditingController();

  @override
  void initState() {
    super.initState();
    final model = MyModel();
    model.addListener(() {
      _text1Ctl.text = model.counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
      FlatButton(
        onPressed: () {
          Provider.of<MyModel>(context, listen: false).updateCounter();
        },
        child: Text('Press me'),
      ),
      // 1st attempt
      // Doesn't work because the listener isn't applied to the instance of the model provided by the provider.
      TextFormField(controller: _text1Ctl),
      // 2nd attempt
      // Works but with `Another exception was thrown: setState() or markNeedsBuild() called during build.` because it changes text via controller (which implies a rebuild) during building.
      Consumer<MyModel>(builder: (context, model, child) {
        _text2Ctl.text = model.counter;
        return TextFormField(controller: _text2Ctl);
      })
    ]));
  }
}

1

There are 1 best solutions below

1
On

Your second example works without any errors when I run it:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyModel extends ChangeNotifier {
  void updateCounter() {
    ++_counter;
    notifyListeners();
  }

  MyModel() {
    _counter = 1;
  }

  int _counter;
  String get counter => _counter.toString();
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyModel(),
      child: MaterialApp(
        title: 'Test',
        home: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var _text2Ctl = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
      FlatButton(
        onPressed: () {
          Provider.of<MyModel>(context, listen: false).updateCounter();
        },
        child: Text('Press me'),
      ),

      // 2nd attempt
      // Works but with `Another exception was thrown: setState() or markNeedsBuild() called during build.` because it changes text via controller (which implies a rebuild) during building.
      Consumer<MyModel>(builder: (context, model, child) {
        _text2Ctl.text = model.counter;
        return TextFormField(controller: _text2Ctl);
      })
    ]));
  }
}

Demo