How to TextFormField in flutter when state changes, using Provider/ChangeNotifier?

Viewed 88

I have an issue with updating text inside TextFormField when using Provider as state management.

I reduced my problem to an abstract one (I removed all the clutter code) and here how it works:

  • there is a someValue in AppState
  • the someValue can be edited via Form->TextFormField
  • the someValue is to be reflected as a title of the AppBar when typing (onChange)
  • the someValue can be updated from external source (in the example it is a button that updates it)
  • when someValue is updated from external source, it MUST be updated in text Form->TextFormField as well

The last one is causing me the problem. Consider the following code:

AppState.dart

import 'package:flutter/foundation.dart';

class AppState extends ChangeNotifier{
  String someValue = '';

  updateSomeValue(String newValue){
    someValue = newValue;
    notifyListeners();
  }
}

main.dart

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

void main() {
  runApp(ChangeNotifierProvider(create: (_) => AppState(), child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  late TextEditingController _controller;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }

  @override
  Widget build(BuildContext context) {
    final provider = Provider.of<AppState>(context);

// following line of code makes it possible for text to be changed by button
// and reflected in TextFormField
// but it causes nasty side effect, that when typing, cursor always goes to beginning of the line
    _controller.text = provider.someValue;

    return Scaffold(
      appBar: AppBar(
        title: Text(provider.someValue),
      ),
      body: Center(
        child: Form(
            key: _formKey,
            child: Column(children: [
              TextFormField(
                controller: _controller,
                onChanged: (value) {
                  provider.updateSomeValue(value);
                },
              ),
              ElevatedButton(
                  onPressed: () {
                    provider.updateSomeValue('foo_bar');
                  },
                  child: Text('change text external source'))
            ])),
      ),
    );
  }
}

The problem: When I added the line _controller.text = provider.someValue; it fixed the issue of updating TextFormField when button is clicked, but it create new issue, that when typing in TextFormField, it is also triggered, cause carret of text field to move to the beginning of the text field.

How to make it work so the text (value) of a TextFormField can be updated externally, without causing carret issue when typing?

EDIT The answer of Yeasin Sheikh using addListener doesn't quite work (it is hacky) because:

  1. it listens to every event (e.g. onFocus or cursor changed)
  2. it does not take into account situation that EleveatedButton is in different scope than _controller (e.g. is in different widget).
2 Answers

An easy way of doing this by listening TextEditingController, while the TextFormField is the ruler here.

class _MyHomePageState extends State<MyHomePage> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  late TextEditingController _controller;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController()
      ..addListener(() {
        Provider.of<AppState>(context, listen: false)
            .updateSomeValue(_controller.text);
      });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(context.watch<AppState>().someValue),
      ),
      body: Center(
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _controller,
              ),
              ElevatedButton(
                  onPressed: () {
                    _controller.text = 'foo_bar';
                  },
                  child: Text('change text external source'))
            ],
          ),
        ),
      ),
    );
  }
}

Also, you can check riverpod

import 'package:flutter/foundation.dart';

class AppState extends ChangeNotifier
{ 
   TextEditingController _controller=TextEditingController();

   TextEditingController get controller=>_controller();

   String someValue = '';

   updateSomeValue(String newValue)
   {
      someValue = newValue;
      notifyListeners();
    }
    }

main.dart

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

void main() {
runApp(ChangeNotifierProvider(create: (_) => AppState(), child: MyApp()));
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  home: const MyHomePage(),
);
}
}

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

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@override
void dispose() {
super.dispose();
}

@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
final provider = Provider.of<AppState>(context);

return Scaffold(
  appBar: AppBar(
    title: Text(provider.someValue),
  ),
  body: Center(
    child: Form(
        key: _formKey,
        child: Column(children: [
          TextFormField(
            controller: Provider.controller,
            onChanged: (v) {
              provider.updateSomeValue(v);
            },
          ),
          ElevatedButton(
              onPressed: () {
                provider.updateSomeValue('foo_bar');
              },
              child: Text('change text external source'))
        ])),
  ),
);
}
}
Related