Flutter : initial value not updating in Form (FormBuilder) - flutter

_updatePersonalFormScreen(String loginId) async {
if (!DartUtility.isNullEmptyOrWhitespace(loginId)) {
_personalInfo = await _service.getUserPersonalDetails(loginId);
setState(() {
if (_personalInfo != null) {
if(!DartUtility.isNullEmptyList(_personalInfo.getContacts())){
contactList = _personalInfo.getContacts();
}
personalInfoMap = _personalInfo.toPersonalInfoMap();
}
print('personalInfo retrieved object ${_personalInfo.toString()}'); //1
});
}
}
formBuilder widget :
FormBuilder buildFormBuilder(BuildContext context) {
print('personalInfoMap $personalInfoMap'); //2
return FormBuilder(
key: _personalDetailFormKey,
initialValue: personalInfoMap, //3
autovalidate: true,
child: Stack(),
);
}
//line-1 and line-2 printing correct values but at line-3, the initial values are not getting assigned to the form builder textbox
'contactList' is populating correctly and in the same block populating 'personalInfoMap' not working properly as expected
or may value assigned at line-3 need some thing else to be modified to make it work
I have tried working with Future builder as well but no luck. If 'contactList' is working fine and assigned to the form values, so why facing issue in other field ? :(
Could someone please help me on this, What else need to be done here and where its getting wrong.

After 4 5 hour struggle, able to resolved finally, and the saviour is 'Future builder'.
here is the solution,
Instead of directly calling FormBuilder in build method, wrap it inside FutureBuilder
#override
Widget build(BuildContext context) =>SafeArea(
child: Scaffold(
body: Container(
height: MediaQuery.of(context).size.height,
child: FutureBuilder(
future: _getPersonalInfoFormInitialValue(),
builder: (context, snapshot) => snapshot.hasData
? buildFormBuilder(context, snapshot.data) // this provide returned data from _getPersonalInfoFormInitialValue()
: const SizedBox(),
),
),
),
);
Modified formBuilder widget :
FormBuilder buildFormBuilder(BuildContext context, data) {
print('datat ::::$data');
return FormBuilder(
key: _personalDetailFormKey,
initialValue:data, //assigned fetched data
autovalidate: true,
child: Stack(),
);
}

It seems like the value initially loaded can't be changed still the _formKey remains in memory. So we need to prevent initializing first time with null
I use reverpod with flutter form following is the relevant code of rough implementation with watch
Widget build(
BuildContext context,
ScopedReader watch,
) {
final loginId = context.read(selectedLoginId).state; // user id to check if there is a valid data
final user = watch(selectedUser).data?.value; // getting user info we need both
return Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
children: [
(loginId != null && user == null)
? CircularProgressIndicator()
: FormBuilder(
key: _personalDetailFormKey,
initialValue:user, //assigned fetched data
autovalidate: true,
child: Stack(),
),]));}

Struggled for a long time but found the solution to form_builder update problem
class _CommonContentFormState extends State<CommonContentForm>
var commonForm = GlobalKey<FormBuilderState>();
#override
Widget build(BuildContext context) {
// Must create new GlobalKey before building form to update
// with new data from Provider...
commonForm = GlobalKey<FormBuilderState>();
formData = Provider.of<FormData>(context);
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(0,0,20,0),
child: FormBuilder(
key: commonForm,
.....

Related

Expected a value of type 'String', but got one of type 'List<dynamic>' for my DropdownMenu

Got an API call that returns a bunch of data for my app. This particular data set is a Map<String, List<dynamic>>, I'm processing this data to make it usable within my app and passing it around to necessary widgets. I came across his error which makes no sense to me but it is self-explanatory looking at the code I cant see anything.
This code is a part of a bigger code please comment if you want me to add it as it just takes in a few arguments to process the Future and create the Map<String, List<dynamic>>.
This is the code where the error is being thrown (Line:45)
#override
Widget build(BuildContext context) {
return FutureBuilder<Map<String, List<dynamic>>>(
future: options,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: snapshot.data!.values.length,
itemBuilder: ((context, index) {
return DropdownMenu(items: snapshot.data!.values.toList()); //Line: 45
}),
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return const CircularProgressIndicator();
}
},
);
}
This is my DropdownMenu Class
class DropdownMenu extends StatefulWidget {
DropdownMenu({super.key, required this.items});
List<dynamic> items;
#override
State<DropdownMenu> createState() => _DropdownMenuState(items);
}
class _DropdownMenuState extends State<DropdownMenu> {
_DropdownMenuState(this.items);
String? value;
List<dynamic> items;
#override
void initState() {
super.initState();
widget.items = items;
}
#override
Widget build(BuildContext context) {
return Container(
width: 300,
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.black, width: 2)),
child: DropdownButtonHideUnderline(
child: DropdownButton<dynamic>(
value: value,
onChanged: (value) => setState(() => this.value = value),
items: items.map(buildMenuItem).toList(),
),
),
);
}
DropdownMenuItem<dynamic> buildMenuItem(dynamic item) => DropdownMenuItem(
value: item,
child: Text(
item,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
);
}
Error
The following TypeErrorImpl was thrown building DropdownMenu(dirty, state:
_DropdownMenuState#69c5b):
Expected a value of type 'String', but got one of type 'List<dynamic>'
The relevant error-causing widget was:
DropdownMenu
DropdownMenu:file:///C:/Main%20Storage/_GitHub%20Repos/flutter_fontend_client/lib/components/options__dropdown_menu.dart:45:22
After some debugging... I added this piece of code
var result1 = {
for (var value in snapshot.data!.values.toList())
value.first: value
};
print("Values of the snapshot: $result1");
The result is a big awkward and IDK why it like this. It prints out a json style format string {'key': ['keyStrings', 'keyStrings']
Got a different answer from someone in NorDev Discord.
I will show the answer here + keep the accepted answer as both work and I think that people will appreciate that there is multiple ways of solving this.
return DropdownMenu(items: snapshot.data!.values.elementAt(index));
According to your code, your response is a Map with strings as keys and List as values. That means that snapshot.data!.values.toList() is a list with (possibly) multiple List<dynamic> that you are passing to DropdownMenu.
DropdownMenu expects that the elements of the list are of type String but they are not.
I suspect what you want to do is actually get the first list, so you could do
return DropdownMenu(items: snapshot.data!.values.first);

Flutter - Attempting to display a list of tweets fetched from JSON onto ListView

I am attempting to fetch a list of tweets using Flutter's HTTP library and Twitter's API endpoint, and then display the list of tweets onto a ListView widget using a Flutter tweet_ui library. I was able to get the fetching of a list of tweets working correctly and I get back a JSON response. However, I am currently attempting to decode the JSON response.body and save it onto a List to later encode once I pass it to the tweet_ui method I am using. Using the code I've shown below I continue get this red error screen with the error saying "Expected a value of type 'String', but got one of type '_Future < dynamic >'". I've tried multiple times playing around with the different types that are being passed around from function to function and that does not seem to work. I've also attempted to wrap the widget around a future builder, and that did not seem to work as well. Any idea what I could possibly be doing wrong?
Future getTweetJson() async {
Map<String, String> params = {
'exclude': 'retweets',
'expansions':
'attachments.poll_ids,attachments.media_keys,author_id,entities.mentions.username,geo.place_id,in_reply_to_user_id,referenced_tweets.id,referenced_tweets.id.author_id',
'tweet.fields':
'attachments,author_id,context_annotations,conversation_id,created_at,entities,geo,id,in_reply_to_user_id,lang,possibly_sensitive,public_metrics,reply_settings,source,text',
'user.fields':
'created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified',
'place.fields':
'contained_within,country,country_code,full_name,geo,id,name,place_type',
'media.fields':
'duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics,non_public_metrics,organic_metrics,promoted_metrics'
};
var response = await http.get(
Uri.https('api.twitter.com', '2/users/IDnumber/tweets', params),
headers: {
"Authorization":
"Bearer bearerToken"
});
String jsonData = response.body;
return jsonData;
}
Function used to fetch tweets using HTTP library from Twitter API
List getListOfTweets(var jsonData) {
List<dynamic> tweets = [];
tweets = convert.jsonDecode(jsonData);
return tweets;
}
Function used to hopefully convert each tweet fetched in the Json to be added to the list.
List<dynamic> tweets = handler.getListOfTweets(handler.getTweetJson());
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Tweets")),
body: Column(children: <Widget>[
Expanded(
child: ListView.builder(
itemCount: tweets.length,
itemBuilder: (context, index) {
return Container(
child: Column(
children: [
EmbeddedTweetView.fromTweet(
Tweet.fromRawJson(
convert.jsonEncode(tweets[index]),
),
)
],
));
}),
),
]),
);
}
Widget using ListView
var tweetJson = handler.getTweetJson();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Tweets")),
body: Container(
child: Card(
child: FutureBuilder(
future: tweetJson,
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.data == null) {
return Container(
child: const Center(
child: Text('Loading. . .'),
),
);
} else {
var tweets =
List<String>.from(convert.jsonDecode(snapshot.data));
return ListView.builder(
itemCount: tweets.length,
itemBuilder: (context, index) {
return Container(
child: Column(
children: [
EmbeddedTweetView.fromTweet(
Tweet.fromRawJson(
convert.jsonEncode(tweets[index]),
),
)
],
));
});
}
},
),
),
),
);
}
Alternative Widget using Future Builder
I suggest to
Create a class model for tweet
Get the json data from api then decode it to json ,after map it to the
created class and then convert it to list (A future builder returning a list
of tweets) or if you use state management (skip to step 4).
Use a state management ie. Provider or Getx
The following is if you use provider as state management
if your using provider use change notifier and save the list of tweets to a list datatype something like List<your_tweet_class> and now you can access it using Provider.of(context) with the class specified

Provider updates variable, but the old value is passed into function that depend on it

I am using a provider to store the value of input fields in a form. On save() method the TextFields call a method on the FormInputProvider that updates the variable inputAnn that holds updated input values.
_inputAnn is listening to the variable inputAnn of the provider.
When I call the function _upDateAnnouncementProvider I am adding the Announcement _inputAnn to a List of Announcements, that contains the form input information. However the value of _inputAnn is not updated by the inputAnn provider.
I tried Therefore inserting an asynchronous function:
The 2 print messages outside the _upDateAnnouncementProvider function are updated, but the print message inside the async function remains the old one.
Can anyone help? If necessary I can give more of the code.
Thanks.
Widget build(BuildContext context) {
var _inputAnn = Provider.of<FormInputProvider>(context).inputAnn;
print('FORM SCREEN BUILD BEFORE : images: ${_inputAnn.images.first}');
Future<void> _upDateAnnouncementProvider() async {
Future.delayed(Duration.zero).then((value) {
Provider.of<Announcements>(context, listen: false).addItem(_inputAnn);
print('INSIDE UPDATE PROVIDER: images: ${_inputAnn.images.first}');
});
}
print('FORM SCREEN BUILD: images: ${_inputAnn.images.first}');
loadHomeTypeList();
return Scaffold(
appBar: AppBar(
title: Text('Aggiungi Annuncio'),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: FormBody(
scrollController: _scrollController,
formKey: _formKey,
getFormFields: () => _getFormFields(context, _inputAnn),
saveForm: _saveForm,
updateProvider: _upDateAnnouncementProvider,
),
),
);
}
By passing the Provider Object to the function outside of the build, the code now works correctly.
void _upDateAnnouncementProvider(FormInputProvider _inputAnnData) {
Provider.of<Announcements>(context, listen: false)
.addItem(_inputAnnData.inputAnn);
print(
'INSIDE UPDATE PROVIDER: images: ${_inputAnnData.inputAnn.images.first}');
}
#override
Widget build(BuildContext context) {
final _inputAnnData = Provider.of<FormInputProvider>(context);
var _inputAnn = _inputAnnData.inputAnn;
print('FORM SCREEN BUILD: images: ${_inputAnn.images.first}');
loadHomeTypeList();
return Scaffold(
appBar: AppBar(
title: Text('Aggiungi Annuncio'),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: FormBody(
scrollController: _scrollController,
formKey: _formKey,
getFormFields: () => _getFormFields(context, _inputAnn),
saveForm: _saveForm,
updateProvider: () => _upDateAnnouncementProvider(_inputAnnData),
),
),
);
}

Flutter: Prevent executed feturebuilder when setState is occurred

I am trying to load DropDownMenu inside Future builder.In my widget i have a Column. Inside Column I have a few widget :
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Row(),
Divider(),
Container(),
...widget._detailsModel.data.appletActions.map((item) {
.....
...item.appletInputs.map((inputs) {
FutureBuilder(
future: MyToolsProvider()
.getDropDownConfiges(inputs.dataUrl),
builder:
(ctx,AsyncSnapshot<DropDownModel.DropDownConfigToolsModle>snapshot) {
if (!snapshot.hasData ||
snapshot.connectionState ==
ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasData &&
snapshot.connectionState ==
ConnectionState.done) {
_dropDown = snapshot.data.data[0];
return DropdownButton<DropDownModel.DataModle>(
hint: Text("Select Item"),
value: _dropDown,
onChanged: (data) {
setState(() {
_dropDown = data;
});
},
items: snapshot.data.data.map((item) {
return DropdownMenuItem<
DropDownModel.DataModle>(
value: item,
child: Row(
children: <Widget>[
Icon(Icons.title),
SizedBox(
width: 10,
),
Text(
item.title,
style: TextStyle(
color: Colors.black),
),
],
),
);
}).toList(),
);
} else {
return Center(
child: Text('failed to load'),
);
}
}),
}
}
]
As you can see i have FutureBuilder inside a loop to show DropdownButton.everything is ok and code works as a charm but my problem is :
onChanged: (data) {
setState(() {
_dropDown = data;
})
every time setState called, future: MyToolsProvider().getDropDownConfiges(inputs.dataUrl), is executed and
_dropDown = snapshot.data.data[0]; again initialized and it get back in a first time .
It is not possible declared MyToolsProvider().getDropDownConfiges(inputs.dataUrl), in initState() method because inputs.dataUrl it is not accessible there.
How can i fixed that?
Updating parent state from within a builder is anti-pattern here. To reduce future errors and conflicts I recommend to wrap the parts that use and update _dropDown variable as a statefull widget.
Afterward the builder is just responsible of selecting correct widget based on future results and separated widget will only update itself based on interactions. Then hopefully many current and potential errors will disappear.
Do one thing, change this
_dropDown = snapshot.data.data[0];
to
_dropDown ??= snapshot.data.data[0];
What this will do is, it will check if _dropDown is null then assign it with value otherwise it won't.

TextField reloads FutureBuilder when pressed/left in Flutter

The user can either enter the answer with InputChips or manually type it in the TextField. When I try with InputChips, the correct answer is not detected. When I try to manually type it, the FutureBuilder reloads when I enter and leave the TextField. What is the reason?
The Future function should only be called once because it fetches a random document from Firestore, splits the String and scrambles the different pieces. It is some form of quiz.
class _buildPhrases extends State<PhrasesSession>{
TextEditingController _c;
String _text = "initial";
#override
void initState(){
_c = new TextEditingController();
super.initState();
}
#override
void dispose(){
_c?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final Arguments args = ModalRoute.of(context).settings.arguments;
var height = MediaQuery.of(context).size.height;
var width = MediaQuery.of(context).size.width;
// TODO: implement build
return Scaffold(
body: Column(
children: <Widget>[
Flexible(flex: 2, child: _buildRest(context),),
Flexible(flex: 5,
child: FutureBuilder(
future: getEverything(args.colName),
builder: (context, snapshot){
if(!snapshot.hasData){
return Center(child: CircularProgressIndicator(),);
}else{
return Column(
children: <Widget>[
Flexible(flex: 1, child: Text(snapshot.data[1]),),
Divider(),
Flexible(flex: 2, child: Container(
child: TextField(
onChanged: (t){
_text += "$t ";
if(_c.text == snapshot.data[0]){
return print("CORRECT ANSWER");
}
},
controller: _c,
textAlign: TextAlign.center,
enabled: true,
),
),),
Flexible(flex: 3,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: snapshot.data.length - 2,
itemBuilder: (context, index){
if(index>snapshot.data.length - 2){
return null;
}else{
return Padding(
padding: const EdgeInsets.all(4.0),
child: InputChip(
label: Text(snapshot.data[index + 2]),
onPressed: (){
_c.text += "${snapshot.data[index + 2]} ";
},
),
);
}
},
))
],
);
}
},
),)
],
)
);
}
}
Let's solve this in parts.
When I try to manually type it the FutureBuilder reloads when I enter and leave the TextField. What is the reason?
This is hapenning because when the keyboard is showing or hidding the flutter framework calls build method of your widget and this default behavior is the reason why your FutureBuilder is realoading. You should avoid call network methods inside build method and I advise you to use BLoC pattern to handle state of your widget.
My Future needs the String that is passed from another route, though. See the Arguments args = .... Any idea how I get it in the initState?
Well if you need context instance to get this String you can't access current context inside initState method because your widget isn't full initialized yet. A simple way to solve this in your case but not the best is verify if the data was already fetched from network or not.
Future _myNetworkFuture; // declare this as member of your stateWidgetClass
Widget build(BuildContext context){
final Arguments args = ModalRoute.of(context).settings.arguments;
var height = MediaQuery.of(context).size.height;
var width = MediaQuery.of(context).size.width;
// this line says if(_myNetworkFuture == null) do the thing.
_myNetworkFuture ??= getEverything(args.colName);
return ...
Flexible(flex: 5,
child: FutureBuilder(
future: _myNetworkFuture,
builder: (context, snapshot){
// ...
}
}
With this approach when flutter framework calls build method if you already fetched the data you don't download the data again. But I really advise you to use BLoC pattern in this kind of situation.