How to solve: setState does not update initialValue of TextFormField widgets

Question

Asked by fferri on November 05, 2021 (source).

I'm totally new to Flutter. I tried to make a login form with an option to remember the credentials.

Since SharedPreferences is async, it makes a this task bit tricky: I created an async method -started in the constructor- to asynchronously obtain the SharedPreferences instance, read some values, then call setState() writing the new state1.

But when setState() is called, and the new state written, the UI does not update (i.e. the text fields remain blank).

The build() method is something like:

@override
Widget build(BuildContext context) {
  developer.log("build called: rememberCredentials=$rememberCredentials, username=$username, password=$password");
  return (... TextFormField(
    initialValue: username,
    onChanged: (String value) {username = value;}
  ) ...);
}

I added a couple of developer.log() calls to see when build() is called, and if saved preferences are actually read (they do):

[log] build called: rememberCredentials=false, username=, password=
[log] read prefs: rememberCredentials=true, username=john, password=1234
[log] build called: rememberCredentials=true, username=john, password=1234
[log] build called: rememberCredentials=true, username=john, password=1234

This is the relevant code:

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

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

class _LoginFormState extends State<LoginForm> {
  String username = "";
  String password = "";
  bool rememberCredentials = false;

  _LoginFormState() {
    readSavedCredentials();
  }

  void readSavedCredentials() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      rememberCredentials = prefs.getBool("rememberCredentials") ?? false;
      username = prefs.getString("username") ?? "";
      password = prefs.getString("password") ?? "";
      developer.log("read prefs: rememberCredentials=$rememberCredentials, username=$username, password=$password");
    });
  }

  void login() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setBool("rememberCredentials", rememberCredentials);
    await prefs.setString("username", rememberCredentials ? username : "");
    await prefs.setString("password", rememberCredentials ? password : "");
    final resp = await http.get(
      Uri.parse('https://127.0.0.1:56873/?action=login&user=$username&pass=${md5hex(password)}')
    );
    if(resp.statusCode == 200) {
      Map o = jsonDecode(resp.body);
      if(o["status"] == "AUTH_OK") {
        developer.log(jsonEncode(o));
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => FilesView(o["base_url"], o["files"])),
        );
      } else {
        // TODO: report error
      }
    } else {
      // TODO: report error
    }
  }

  @override
  Widget build(BuildContext context) {
    developer.log("build called: rememberCredentials=$rememberCredentials, username=$username, password=$password");
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(title: Text(title())),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Padding(
              padding: EdgeInsets.all(15),
              child: TextFormField(
                  initialValue: username,
                  onChanged: (String value) {
                    username = value;
                  },
                  decoration: InputDecoration(
                    border: OutlineInputBorder(),
                    labelText: 'Username',
                  )
              )
          ),
          Padding(
              padding: EdgeInsets.all(15),
              child: TextFormField(
                  initialValue: password,
                  onChanged: (String value) {
                    password = value;
                  },
                  obscureText: true,
                  decoration: InputDecoration(
                    border: OutlineInputBorder(),
                    labelText: 'Password',
                  )
              )
          ),
          Padding(
            padding: EdgeInsets.fromLTRB(0, 10, 0, 30),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Checkbox(
                    value: rememberCredentials,
                    onChanged: (bool? value) {
                      setState(() {
                        rememberCredentials = value!;
                      });
                    }),
                Text('Remember credentials'),
              ],
            ),
          ),
          Container(
            width: 220,
            decoration: BoxDecoration(
              color: Colors.blue,
              borderRadius: BorderRadius.circular(20),
            ),
            child: TextButton(
              onPressed: () {
                login();
              },
              child: Padding(
                padding: EdgeInsets.all(10),
                child: Text(
                  'Login',
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
        ]
      )
    );
  }
}

Since I'm totally new to Flutter, I may have made several mistakes. Any advice on the design is very welcome :)


1: another option was to use FutureBuilder, but it is messier, and it would delay UI creation... I prefer showing the UI soon, and changing fields values later.

Answer

Question answered by developerjamiu (source).

Instead of using using the String username to hold your value and onChanged to change it, you can use a text editing controller so

String username = '';

becomes

TextEditingController usernameController = TextEditingController();

Then in your TextFormFields

TextFormField(
  initialValue: username,
  onChanged: (String value) {
    username = value;
  },
  ...
)

becomes

TextFormField(
  controller: usernameController,
  ...
)

Lastly, in your readSavedCredentials() method you can set the text (username) using the text editing controller

void readSavedCredentials() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  setState(() {
    ...
    usernameController.text = prefs.getString("username") ?? "";
    ...
  });
}
DART FLUTTER
SHARE: