Flutter_form_builder: Entered text disappears on scrolling, in FormBuilderTextField

Created on 27 May 2020  路  6Comments  路  Source: danvick/flutter_form_builder

I am using flutter_form_builder 3.10.1. The issue is that after entering text input in a FormBuilderTextField, the text disappears when I scroll the ListView.

See the short video: https://vimeo.com/user116509358/review/423244392/0e9689694b

My code:
`
import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:intl/intl.dart';
import 'package:myopipclinic/helpers/network.dart';

import 'login_screen.dart';

class RegisterAndAddAppointment extends StatefulWidget {
static const String id = 'RegisterAndAddAppointment';
String urlsuffix;
int clinicId;
int docID;
RegisterAndAddAppointment({this.urlsuffix, this.clinicId, this.docID});

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

class _RegisterAndAddAppointmentState extends State {
final GlobalKey _fbKey = GlobalKey();
Network connection = Network();
String selectedSlot;
List slots;
List slotList;
int clinicId;
int docID;
var selectedDate;
var urlsuffix;
bool showBookButton;

@override
void initState() {
slots = [];
slotList = [];
connection = Network();
clinicId = widget.clinicId;
docID = widget.docID;
urlsuffix = widget.urlsuffix;
showBookButton = false;
var now = DateTime.now();
selectedDate = DateFormat("dd-MM-yy").format(DateTime.now());

print("Got clinicID:$clinicId docID:$docID");

super.initState();

}

Future getSlots() async {
print("In BookAppointment::getSlots");
slotList = [];
selectedSlot = null;
bool resp = await connection.getUserNamePwd();
if (resp == false) {
Navigator.pushNamed(context, LoginScreen.id);
}

String urlSuffix = 'getslots/clinic/$clinicId/doctor/$docID';
var response = await connection.getSlots(
  urlsuffix: urlSuffix,
  clinicID: clinicId,
  docID: docID,
  chosenDate: selectedDate,
);

setState(() {
  slots = jsonDecode(response);
  print("slots: $slots ${slots.runtimeType}");
  print("response is $response");
});

}

Future saveRegistration(
{var enteredData,
BuildContext context,
bool mustCheckin,
bool addAppointment}) async {
print('urlsuffix is ${urlsuffix}');
if (mustCheckin != null && mustCheckin) {
print("Also checking in");
} else {
print("Not checking in");
mustCheckin = false;
}
if (addAppointment != null && addAppointment) {
print("Also adding appointment");
} else {
print("Not adding appointment");
addAppointment = false;
}
print('Entered data');
print(enteredData['phone']);
if (enteredData['dob'] == null) {
enteredData['dob'] = '';
} else {
enteredData['dob'] = enteredData['dob'].toIso8601String();
}
bool resp = await connection.getUserNamePwd();
print("While trying to get username and password, resp: ${resp}");
if (resp == false) {
Navigator.pushNamed(context, LoginScreen.id);
}
await connection.isAuthenticated();
var name = enteredData['name'];
var insurance_number = enteredData['insurance_number'];
var age_yrs = enteredData['age_yrs'];
var age_mnths = enteredData['age_mnths'];
var dob = enteredData['dob'];
var gender = enteredData['gender'];
var phone = enteredData['phone'];
var email = enteredData['email'];
var altphone = enteredData['altphone'];
var address = enteredData['address'];
var marital = enteredData['marital'];
print(
'Sending parameters: name:$name, insurance_number:$insurance_number age_yrs:$age_yrs age_mnths:$age_mnths dob:$dob gender:$gender phone:$phone email:$email altphone:$altphone address:$address marital:$marital mustCheckin:$mustCheckin addAppointment:$addAppointment');
var response = await connection.sendRegistration(
urlsuffix: widget.urlsuffix,
name: name,
insurance_number: insurance_number,
age_yrs: age_yrs,
age_mnths: age_mnths,
dob: dob,
gender: gender,
phone: phone,
email: email,
altphone: altphone,
address: address,
maritalStatus: marital,
mustCheckin: mustCheckin,
addAppointment: addAppointment,
);

print('response is $response');
response = response.substring(1, response.length - 1);
Scaffold.of(context).showSnackBar(SnackBar(content: Text('$response')));
if (response.contains('"Successfully saved')) {
  showBookButton = true;
}

}

@override
Widget build(BuildContext context) {
var now = DateTime.now();
var today = new DateTime(now.year, now.month, now.day);

List<Widget> chipList = List();
slots.forEach((slot) {
  chipList.add(ChoiceChip(
    selectedColor: Colors.red,
    backgroundColor: Colors.green,
    label: Text(
      slot,
      style: TextStyle(color: Colors.white),
    ),
    selected: selectedSlot == slot,
    onSelected: (bool selected) {
      print("$slot was selected");
      setState(() {
        selectedSlot = slot;
        print(selectedSlot == slot);
        print("selected:$selected slot:$slot selectedSlot:$selectedSlot ");
      });
    },
  ));
});

return Scaffold(
  appBar: AppBar(
    title: Text('Register and Book'),
  ),
  body: Builder(builder: (BuildContext context) {
    return FormBuilder(
      key: _fbKey,
      child: ListView(
        shrinkWrap: true,
        children: [
          Row(
            children: [
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: FormBuilderDateTimePicker(
                    attribute: "appointment_date",
                    inputType: InputType.date,
                    format: DateFormat("dd-MM-yyyy"),
                    initialDatePickerMode: DatePickerMode.day,
                    initialDate: DateTime(1980, 6, 15),
                    initialValue: DateTime(now.year, now.month, now.day),
                    lastDate: DateTime(now.year, now.month + 1, now.day),
                    firstDate: DateTime(now.year - 115, now.month, now.day),
                    decoration: InputDecoration(
                      labelText: "Appointment Date",
                      labelStyle: TextStyle(fontSize: 18),
                    ),
                    onChanged: (newDate) {
                      try {
                        selectedDate =
                            DateFormat("dd-MM-yy").format(newDate);
                        print("New date is $selectedDate");
                        getSlots();
                      } catch (e) {

// Do nothing
}
},
validators: [
// ignore: missing_return
(val) {},
],
),
),
),
RaisedButton(
onPressed: () {
getSlots();
},
child: Text('Get Slots'),
),
],
),
Wrap(
children: chipList,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderTextField(
attribute: "name",
decoration: InputDecoration(labelText: "Name"),
validators: [
FormBuilderValidators.required(
errorText: 'You have to enter patient\'s name'),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderTextField(
attribute: "insurance_number",
decoration: InputDecoration(labelText: "Insurance No:"),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderTextField(
attribute: "age_yrs",
decoration: InputDecoration(
labelText: "Age(in years)",
labelStyle: TextStyle(fontSize: 18),
),
validators: [
FormBuilderValidators.numeric(),
FormBuilderValidators.max(110),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderTextField(
attribute: "age_mnths",
decoration: InputDecoration(
labelText: "Age(in months)",
labelStyle: TextStyle(fontSize: 18),
),
validators: [
FormBuilderValidators.numeric(),
FormBuilderValidators.max(12),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderDateTimePicker(
attribute: "dob",
inputType: InputType.date,
format: DateFormat("dd-MM-yyyy"),
initialDatePickerMode: DatePickerMode.day,
initialDate: DateTime(1980, 6, 15),
lastDate: DateTime(now.year, now.month, now.day),
firstDate: DateTime(now.year - 115, now.month, now.day),
decoration: InputDecoration(
labelText: "Date of Birth",
labelStyle: TextStyle(fontSize: 18),
),
validators: [
(val) {},
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderDropdown(
attribute: "gender",
decoration: InputDecoration(
labelText: "Gender",
labelStyle: TextStyle(fontSize: 22),
),
hint: Text(
'Select Gender',
// style: TextStyle(fontSize: 22),
),
validators: [FormBuilderValidators.required()],
initialValue: 'Male',
items: ['Male', 'Female', 'Other']
.map((gender) => DropdownMenuItem(
value: gender, child: Text("$gender")))
.toList(),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderTextField(
attribute: "phone",
decoration: InputDecoration(
labelText: "Phone Number",
labelStyle: TextStyle(fontSize: 18),
),
validators: [
FormBuilderValidators.numeric(),
FormBuilderValidators.required(
errorText: 'Phone number is required.')
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderTextField(
// controller: _emailController,
keyboardType: TextInputType.emailAddress,
attribute: "email",
decoration: InputDecoration(
labelText: "Email",
labelStyle: TextStyle(fontSize: 18),
),
validators: [
FormBuilderValidators.email(),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderTextField(
// controller: _phoneController,
attribute: "altphone",
decoration: InputDecoration(
labelText: "Alternate Phone",
labelStyle: TextStyle(fontSize: 18),
),
validators: [],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderTextField(
attribute: "address",
decoration: InputDecoration(labelText: "Address"),
validators: [],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilderDropdown(
attribute: "marital",
decoration: InputDecoration(
labelText: "Marital Status",
labelStyle: TextStyle(fontSize: 22),
),
// initialValue: 'Male',
hint: Text(
'Select',
),
initialValue: 'Unmarried',
validators: [FormBuilderValidators.required()],
items: ['Married', 'Unmarried', 'Other']
.map((gender) => DropdownMenuItem(
value: gender, child: Text("$gender")))
.toList(),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
showBookButton
? Container()
: Padding(
padding: EdgeInsets.symmetric(horizontal: 5),
child: RaisedButton(
color: Colors.blue,
onPressed: () {
if (_fbKey.currentState.saveAndValidate()) {
print(_fbKey.currentState.value);
saveRegistration(
enteredData: _fbKey.currentState.value,
context: context,
addAppointment: true,
);
}
},
child: Text('Register'),
),
),
!showBookButton
? Container()
: Padding(
padding: EdgeInsets.symmetric(horizontal: 5),
child: RaisedButton(
color: Colors.blue,
onPressed: () {
if (_fbKey.currentState.saveAndValidate()) {
print(_fbKey.currentState.value);
saveRegistration(
enteredData: _fbKey.currentState.value,
context: context,
addAppointment: true,
);
}
},
child: Text('Book appointment'),
),
),
],
),
SizedBox(
height: 20,
)
],
),
);
}),
);
}
}

`

bug

Most helpful comment

Hi @droidzone ,

I've run into the same issue (I think). It's unexpected (to me) that FormBuilder doesn't restore the FormBuilderTextField's state when scrolling back to it. FormBuilder utilizes the initialValue paramter to initialize the value of FormBuilderTextField. Scrolling back to the field seems like a similar operation where FormBuilder provides the last know state of the field rather than the initialValue.

I've found a thing that makes it work for me. I use the onChanged parameter to captuer the latest value and replace the value in initialValues.

FormBuilder(
    key: _formKey,
    initialValue: initialValues,
    child: ...

        FormBuilderTextField(
            onChanged: (value) {
                initialValues["name"] = value;
            },
            ...
        )

All 6 comments

Hi @droidzone,
First I don't think the video link you provided seems to work.

I think (and I'm speculating here) that this may be caused by the ListView recycling its child Widgets as you scroll because this is its behavior in order to optimize computing resources.

If this is the case, kindly consider either:

  1. Using Keys to uniquely identify items within your ListView or,
  2. Using a Column instead of a ListView

Hello Dan, Thank you for responding.

The updated video link is https://www.youtube.com/watch?v=xkkr92pnPmY
I tried using keys, but still the text that I enter keeps disappearing.
Column doesnt work for me, since I have several text fields, and I need the user to scroll down and fill them.

    child: ListView(
                children: [
                Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: FormBuilderTextField(
                    key: UniqueKey(),
                    attribute: "name",
                    decoration: InputDecoration(labelText: "Name"),
                    validators: [
                        FormBuilderValidators.required(
                            errorText: 'You have to enter patient\'s name'),
                    ],
                    ),
                ),
                Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: FormBuilderTextField(
                    key: UniqueKey(),
                    attribute: "insurance_number",
                    decoration: InputDecoration(labelText: "Insurance No:"),
                    ),
                ),

Hi @droidzone ,

I've run into the same issue (I think). It's unexpected (to me) that FormBuilder doesn't restore the FormBuilderTextField's state when scrolling back to it. FormBuilder utilizes the initialValue paramter to initialize the value of FormBuilderTextField. Scrolling back to the field seems like a similar operation where FormBuilder provides the last know state of the field rather than the initialValue.

I've found a thing that makes it work for me. I use the onChanged parameter to captuer the latest value and replace the value in initialValues.

FormBuilder(
    key: _formKey,
    initialValue: initialValues,
    child: ...

        FormBuilderTextField(
            onChanged: (value) {
                initialValues["name"] = value;
            },
            ...
        )

@SpencerRiddering Thanks for your tip, it solved me the issue too ;)

Hi,

I'm having the same issue using the FormBuilderDateTimePicker, the workaround mentioned by @SpencerRiddering works for me as well.

For my case, my form has two steps. After selected the date and navigating to the next step and backing, the field back to initialState.

I/flutter (13785): XXXXX FBDTP initState CALLED.
GO to NEXT screen and BACK, the widget will be rebuilt here
I/flutter (13785): XXXXX FBDTP initState CALLED. <- at this moment the value is replaced by initState using the InitialValue and ignoring the state stored by the user on _textFieldController

Maybe it should check on initState if the value was changed by the user if it was it should ignore the initialValue.

Editing: Forgot to mention that if I opt to not use a initialValue, it is replacing with an empty string.

Thanks!

Was this page helpful?
0 / 5 - 0 ratings