I'm making a question answer application to pass time.
There are questions in a question list. The code below shows for each question. A question can have multiple choices but only 1 answer.
I'm having trouble where everytime I type in the TextFormField, the cursor resets to the beginning. How can i avoid that? I need to use setState so that the Text Widget updates in realtime.
class Question{
int answer;
List<Answer> answers;
}
class Answer{
int index;
String answer;
}
class EditQuestionWidget extends StatefulWidget {
final List<Question> questions;
final int index;
EditQuestionWidget(this.questions, this.index);
#override
_EditQuestionWidgetState createState() => _EditQuestionWidgetState();
}
class _EditQuestionWidgetState extends State<EditQuestionWidget> {
#override
Widget build(BuildContext context) {
return Card( //For Each Question
child: ExpansionTile(
title: Text(widget.questions[widget.index].answer != null ? widget.questions[widget.index].answers[widget.questions[widget.index].answer].answer ?? 'Untitled Answer' : 'Untitled Answer'),
children: <Widget>[
ListView.builder( //For Each answer choice
itemCount: widget.questions[widget.index].answers.length,
itemBuilder: (context, i) {
TextEditingController answerController = TextEditingController(text: widget.questions[widget.index].answers[i].answer);
return Row(
children: <Widget>[
Expanded(
child: RadioListTile( //Select Answer
value: widget.questions[widget.index].answers[i].index,
groupValue: widget.questions[widget.index].answer,
onChanged: (int value) => setState(() => widget.questions[widget.index].answer = value),
title: TextFormField( //change answer choice text
controller: answerController,
onChanged: (text) {
setState(() {widget.questions[widget.index].answers[i].answer = text;}); // The cursor returns to beginning after executing this.
},
),
),
),
IconButton( //Remove Answer Function
onPressed: () {
//This method Works as intended.
if (widget.questions[widget.index].answers[i].index == widget.questions[widget.index].answer) widget.questions[widget.index].answer = null;
String answer = widget.questions[widget.index].answer != null ? widget.questions[widget.index].answers[widget.questions[widget.index].answer].answer : '';
widget.questions[widget.index].answers.remove(widget.questions[widget.index].answers[i]);
if (widget.questions[widget.index].answers.isEmpty) {
widget.questions[widget.index].answer = widget.questions[widget.index].answers.length;
widget.questions[widget.index].answers.add(new Answer(index: widget.questions[widget.index].answers.length));
}
for (int ii = 0; ii < widget.questions[widget.index].answers.length; ii++) {
widget.questions[widget.index].answers[ii].index = ii;
if (widget.questions[widget.index].answer != null && answer == widget.questions[widget.index].answers[ii].answer) widget.questions[widget.index].answer = ii;
}
setState(() {});
},
),
],
);
},
),
],
),
);
}
}
You're updating the value of answerController every time the text changes, and when this update happens the cursor goes to the beginning.
To solve that you can create a list of TextEditingControllers outside the builder:
List<TextEditingController> controllers = [];
And then add a new Controller to the list when a new field is created:
itemBuilder: (context, i) {
if(controllers.length != widget.questions[widget.index].answers.length){
controllers.add(TextEditingController()); //condition to prevent the creation of infinity Controllers
}
And finally pass the controller to the TextField:
controller: controllers[i],
Related
I read the questions that were asked in this regard, but I did not get an answer or I made a mistake somewhere
According to the code I have, when clicking on each panel, the panels that were already open are not closed. And this is because I use one variable for all of them
How can this be solved?
bool isExpanded = false;
ListView.builder(
itemBuilder: (context, index) {
return Column(
children: [
ExpansionPanelList(
expansionCallback: (panelIndex, isExpanded) {
setState(() {
isExpandeder = !isExpanded;
});
},
children: [
ExpansionPanel(
headerBuilder: (context, isExpanded) {
return Row(
children: [
Text("Header"),
const Spacer(),
Text("username")
],
);
},
body: Column(
children: [
Text(
_signals![index].pair.toString(),
),
],
),
isExpanded: isExpanded,
),
],
)
],
);
},
itemCount: _signals!.length,
),
You don't need to have a list of bool values to store expanded values since you have only one value maximum. You can store index of currently expanded tile.
In your ListView you actually create multiple ExpansionPanelLists with one element each, instead of one list with all of them. You don't need a ListView at all.
You can try the following code in Dartpad, it does what you want.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<MyApp> createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
var content = ['text', 'more text', 'another text', 'random text'];
int? expandedItemIndex;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: ExpansionPanelList(
expansionCallback: (panelIndex, isExpanded) {
setState(() {
if (!isExpanded) {
expandedItemIndex = panelIndex;
} else {
expandedItemIndex = null;
}
});
},
children: [
for (var i = 0; i < content.length; i++)
ExpansionPanel(
headerBuilder: (context, isExpanded) {
return Text("Header $i");
},
body: Text(content[i]),
isExpanded: expandedItemIndex == i,
),
],
),
),
),
);
}
}
And this is because I use one variable for all of them How can this be
solved?
You already answered your question. You need a List of booleans or you add a isExpanded boolean in your signals object. If you have only variable, all the ExpansionPanels will behave the same.
You could do something like this:
List<bool> isExpanded = [];
for(int i = 0; i < _signals.length; i++){
isExpanded.add(false);
}
So every expanded panel is collapsed at first time.
Now in your ExpansionPanel you just read the bool at position index:
isExpanded: isExpanded[index],
And to switch your expansion panel state just invert the bool at the position in the isExpanded list:
expansionCallback: (panelIndex, isExpanded) {
setState(() {
isExpanded[index] = !isExpanded[index];
});
},
I am using the flutter_riverpod package. I'm using it for a search field and it's showing an error when searching for something. I asked a question about the error but couldn't fix it. Is there any other way to create a search field in Flutter using the flutter_riverpod package?
Here is some code:
User interface code:
TextFormField(
…
onChanged: (search) => controller.updateSearch(search),
onSaved: (search) {
search == null ? null : controller.updateSearch(search);
},
),
itemBuilder: (context, index) {
if (mounted) {
return name.contains(state.search) ? ListTile(title: Text(name)) : Container();
}
return Container();
},
Controller code:
class Controller extends StateNotifier<State> {
Controller() : super(State());
void updateSearch(String search) => state = state.copyWith(search: search);
}
final controllerProvider = StateNotifierProvider.autoDispose< Controller, State>((ref) {
return Controller();
});
State code:
class State {
State({this.search = "", this.value = const AsyncValue.data(null)});
final String search;
final AsyncValue<void> value;
bool get isLoading => value.isLoading;
State copyWith({String? search, AsyncValue<void>? value}) {
return State(search: search ?? this.search, value: value ?? this.value);
}
}
Is there something wrong with the code above? If yes, what is the way to create the seach field using the flutter_riverpod package? If not, why am I getting the error message (go to this question to see the error message)?
If I can't create a search field using the flutter_riverpod package in Flutter, how can I create a search field (I hope maybe without using any package and without using setState function)?
Feel free to comment if you need more information!
How to fix this error? I would appreciate any help. Thank you in advance!
Update:
We can discuss in this room.
this may not answer directly the question but it may give another variation on how to restructure the code.
To track the loading state, from the controller:
class Controller extends StateNotifier<AsyncValue<void>> {
Controller({required this.repository}) : super(AsyncData(null));
//this depends on where your search api is located at.
//for this example i use repository because it is mostly where its is.
final SeachRepository repository
Future<void> submitSearch(String search) async {
state = const AsyncLoading();
//`.guard` is used to handle possible errors from your API so no need
// to use `try/catch`
state = await AsyncValue.guard(()=> repository.submitSearch(search));
}
}
final controllerProvider = StateNotifierProvider.autoDispose< Controller, AsyncValue<void>>((ref) {
return Controller();
});
You can use your controller from your UI like this:
final controller = ref.watch(controllerProvider.notifier);
TextFormField(
…
onChanged: (search) => ref.read(searchValueProvider.notifier).state = search,
onSaved: (search) {
search == null ? null : controller.submitSearch(search);
},
),
And for the loading state:
//your loading state
final state = ref.watch(controllerProvider);
//to handle passible error:
ref.listen<AsyncValue>(controllerProvider, (_, state)=> showAlertDialog(context));
itemBuilder: (context, index) {
if (mounted) {
return name.contains(state.search) ? ListTile(title: Text(name)) : Container();
}
return state.isLoading ? CircularProgressIndicator() : Container();
},
for the search variable, you can store it in a StateProvider for easy access:
final searchValueProvider = StateProvider.autodispose((ref)=> '');
Try the following code:
class FooSearchRiverpod extends ConsumerWidget {
final controller = StreamController<String>();
late final provider = obtainProvider(controller);
#override
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<List<String>> list = ref.watch(provider);
return Column(
children: [
TextField(
onChanged: controller.add,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search), hintText: 'search...'),
),
Expanded(
child: list.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Card(elevation: 4, child: Text(err.toString())),
),
),
data: (list) => AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
child: ListView.builder(
itemCount: list.length,
itemBuilder: (ctx, i) => ListTile(
key: UniqueKey(),
title: Text(list[i]),
onTap: () => debugPrint('onTtap: ${list[i]}'),
),
),
),
),
),
],
);
}
obtainProvider(StreamController<String> controller) {
return StreamProvider.autoDispose<List<String>>((ref) => controller.stream
.debounce(const Duration(milliseconds: 1000))
.asyncMap(_getAPI));
}
final list = ['foo', 'foo 2', 'bar', 'bar 2', 'spam'];
Future<List<String>> _getAPI(String query) async {
debugPrint('_getAPI: |$query|');
if (query.isEmpty) return [];
return list.where((s) => s.contains(query)).toList();
}
}
I have a listview to which am applying a filter , the filter works but the UI won't update the list is unchanged .
this is my implementation
class QrqcOnlineListView extends StatefulWidget {
#override
_QrqcOfflineListViewState createState() => _QrqcOfflineListViewState();
}
List<Qrqc> filteredQrqc = [];
List<Qrqc>? qrqcList = [];
class _QrqcOfflineListViewState extends State<QrqcOnlineListView> {
List<Qrqc>? myList;
String? label;
Future<List<Qrqc>?> getQrqcData() => SharedPreferenceMyQrqc().getListQrqc();
List<Qrqc> filteredQrqc = [];
final _controller = TextEditingController();
String? _searchText;
List<TypeSettings> listTypes = [];
#override
void initState() {
Provider.of<MyQrqcListViewModel>(context, listen: false).fetchMyQrqc();
super.initState();
fetchTypes();
}
fetchTypes() async {
listTypes = (await SettingsViewModel().fetchTypes());
}
#override
Widget build(BuildContext context) {
var myQrqcListViewModel = Provider.of<MyQrqcListViewModel>(context);
myList = myQrqcListViewModel.articlesList;
List<Qrqc>? qrqcList = myList;
filteredQrqc = myList!;
List<Qrqc> listFilter = List.from(qrqcList!);
QrqcDetails? result;
String? type;
void updateList(String value) {
setState(() {
listFilter = qrqcList
.where((element) =>
element.title!.toLowerCase().contains(value.toLowerCase()))
.toList();
});
}
String? setTypeLabel() {
for (int j = 0; j < listFilter.length; j++) {
for (int i = 0; i < listTypes.length; i++) {
if (listTypes[i].id == listFilter[j].typeID) {
listFilter[j].typeName = listTypes[i].label;
}
}
}
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: InputDecoration(
hintText: "Search",
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(4.0),
),
),
),
onChanged: (value) => updateList(value),
),
Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: ListView.builder(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
itemCount: listFilter.length,
itemBuilder: (BuildContext context, int index) {
String? backgroundImage;
String? _setImage() {
setTypeLabel();
if (listFilter[index].typeName == "Delivery") {
backgroundImage = "assets/icons/icon_delivery.png";
} else if (listFilter[index].typeName == "Security") {
backgroundImage = "assets/icons/icon_security.png";
} else if (listFilter[index].typeName == "Quality") {
backgroundImage = "assets/icons/quality.png";
} else if (listFilter[index].typeName == "Cost") {
backgroundImage = "assets/icons/Cost.png";
} else if (listFilter[index].typeName == "People") {
backgroundImage = "assets/icons/people.png";
} else {
backgroundImage = "assets/icons/unknown.png";
}
// print("list types: $qrqcList[index].typeName");
// print("_mTitle: $backgroundImage");
return backgroundImage;
}
return Column(
children: [
QrqcBody(
child: QrqcCard(
child: QrqcCardBody(
color: Colors.orange,
text: listFilter[index].status,
leading: QrqcCardLeaing(imgPath: _setImage()),
trailing: QrqcCardtrailing(
text: listFilter[index].progress.toString(),
percent: listFilter[index].progress.toString(),
),
title: listFilter[index].id.toString(),
subtitle: listFilter[index].title,
chlidren: [
QrqcDetailsCardFirstRow(
product: listFilter[index].productName ?? 'inconnu',
role: listFilter[index].role ?? "inconnu",
),
const SizedBox(height: 10),
QrqcDetailsCardSecondRow(
perim: listFilter[index].perimeterName ?? "inconnu",
date: convertDateTimeDisplay(
listFilter[index].createdAt!),
),
const SizedBox(height: 10),
],
)),
),
],
);
}),
),
],
);
}
}
I don't know what's wrong especially that the filter is working and i got no errors only the keyboard events in the stack trace , if anyone can help i'd be grateful , i've been stuck for a while now
Your issue is that your list is being re-initialized / reset every time in the build method; while you're filter the list correctly inside the setState, when the build method re-executes upon calling setState, you're resetting it again when you do this:
// upon every rebuild, the listFilter gets reset to the original value
List<Qrqc> listFilter = List.from(qrqcList!);
What you could is:
Use the existing property called ** _searchText** and store the value being provided as input by the TextField widget, that when it's empty, then you fetch the whole list, otherwise skip that logic, that way your listFilter remains filtered on the next widget rebuild, kind of like:
In your *updateList method:
void updateList(String value) {
// store the value being provided as input to the filter
// in the property called _searchText
_searchText = value;
setState(() {
listFilter = qrqcList
.where((element) =>
element.title!.toLowerCase().contains(_searchText.toLowerCase()))
.toList();
});
}
Inside the build method:
// check if its empty, that means there's no filter being applied
if (_searchText.isEmpty) {
List<Qrqc> listFilter = List.from(qrqcList!);
}
My Bad
It was problem with my future function HashtagService().getSuggestion('topic'); returning empty List before data is properly loaded. EditTopicPage didn't have any problem.
Original Question
I have text form field inside futurebuilder. When I first open page, future is not loaded. When I click on text field to enter something, future is loaded.
I want future to be loaded when the page is first opened.
class EditTopicPage extends StatefulWidget {
const EditTopicPage({required UserProfileModel userProfile, Key? key}) : _userProfile = userProfile, super(key: key);
final UserProfileModel _userProfile;
#override
_EditTopicPageState createState() => _EditTopicPageState();
}
class _EditTopicPageState extends State<EditTopicPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _controller = TextEditingController();
List<String> _hashList = [];
List<String> _suggestionList = [];
late final Future<List<String>> _future;
bool _disabled = true;
final RegExp hashRegex = RegExp(r'^[a-z|A-Z|ㄱ-ㅎ|ㅏ-ㅣ|가-힣|ㆍ|ᆢ]*$');
#override
void initState() {
_future = HashtagService().getSuggestion('topic');
_hashList = widget._userProfile.topic.cast<String>();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
UserProfileService _userProfileService = UserProfileService();
final Size size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
appBar: TopBar(pageTitle: "관심 대화 주제 수정", context: context),
body: ListView(
padding: EdgeInsets.all(16.sp),
children: [
FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
_suggestionList = List<String>.from(snapshot.data.reversed);
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SingleLineTextFormField(
controller: _controller,
hintText: '어떤 주제로 대화하고 싶으신가요?',
onChanged: (value) {
if (value.length > 1) {
var lastChar = value.substring(
value.length - 1);
if (!hashRegex.hasMatch(lastChar)) {
var newHash = value.substring(
0, value.length - 1);
if (!_hashList.contains(newHash)) {
setState(() {
_hashList.add(newHash);
_disabled = _hashList.isEmpty || (_hashList.length > 3);
});
}
_controller.clear();
}
} // else if (_expHash.length == 3 && value.isNotEmpty) {
_formKey.currentState!.validate();
},
validator: (value) {
if (!hashRegex.hasMatch(value!)) {
return '\u26a0 한글, 영문만 가능해요';
}
return null;
}
),
DefaultSpacing(),
Row(
children: [
Text('추천 : ',
style: TextStyle(
fontSize: 10.sp,
color: Colors.grey[800],
),
),
Wrap(
spacing: 6.0,
runSpacing: 6.0,
children: _suggestionList.map((suggestion) =>
HashtagSuggestionChip(
suggestion: suggestion,
type: 'topic',
onPressed: () {
if (!_hashList.contains(suggestion)) {
setState(() {
_hashList.add(suggestion);
_disabled = _hashList.isEmpty || (_hashList.length > 3);
});
}
},
)).toList(),
),
],
),
DefaultSpacing(),
Wrap(
spacing: 6.0,
runSpacing: 6.0,
children: _hashList.map((hashtag) =>
HashtagChip(
hashtag: hashtag,
type: 'topic',
onDelete: () {
setState(() {
_hashList.remove(hashtag);
_formKey.currentState!.validate();
_disabled = _hashList.isEmpty || (_hashList.length > 3);
});
},
)).toList()
),
]),
);
}
return CircularProgressIndicator();
}
),
],
),
floatingActionButton: FloatingSaveButton(context: context,
text: '저장',
width: size.width * 0.9,
disabled: _disabled,
onPressed: () async {
_userProfileService.updateTopicHashtag(hashList: _hashList);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(SaveSnackBar());
}),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}
Result: when the page is first opened
Neither hashtag nor progress indicator is shown.
After text form field is selected
I searched similar questions in stackoverflow, but none of the answers solved my problem.
Try like this
#override
void initState() {
setState(() {
_future = HashtagService().getSuggestion('topic');
_hashList = widget._userProfile.topic.cast<String>();
});
super.initState();
}
so your _future is async so the widget will be loaded before loading the service
One of the requirements of my current project is a multi-page sign-up form, with each page performing its own independent validation. The way I ended up deciding to implement this is by splitting it up into several smaller forms, which you can pass some functions in as parameters to be executed when the form validation and/or value change runs. Below is an example of one of the "mini-forms" I created to implement this. This one collects information about the user's name.
import "package:flutter/material.dart";
class SignUpNameForm extends StatefulWidget {
final Function(String) onFirstNameChange;
final Function(String) onLastNameChange;
final Function(bool) onFirstNameValidationStatusUpdate;
final Function(bool) onLastNameValidationStatusUpdate;
final String firstNameInitialValue;
final String lastNameInitialValue;
SignUpNameForm(
{this.onFirstNameChange,
this.onLastNameChange,
this.onFirstNameValidationStatusUpdate,
this.onLastNameValidationStatusUpdate,
this.firstNameInitialValue,
this.lastNameInitialValue});
#override
_SignUpNameFormState createState() => _SignUpNameFormState();
}
class _SignUpNameFormState extends State<SignUpNameForm> {
final _formKey = GlobalKey<FormState>();
TextEditingController _firstName;
TextEditingController _lastName;
bool _editedFirstNameField;
bool _editedLastNameField;
#override
Widget build(BuildContext context) {
_firstName.text = widget.firstNameInitialValue;
_lastName.text = widget.lastNameInitialValue;
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
controller: _firstName,
autovalidate: true,
decoration: InputDecoration(
hintText: "First Name",
),
keyboardType: TextInputType.text,
onChanged: (String value) {
_editedFirstNameField = true;
widget.onFirstNameChange(value);
},
validator: (String value) {
String error;
if (_editedFirstNameField) {
error = value.isEmpty ? "This field is required" : null;
bool isValid = error == null;
widget.onFirstNameValidationStatusUpdate(isValid);
}
return error;
},
),
TextFormField(
controller: _lastName,
autovalidate: true,
decoration: InputDecoration(
hintText: "Last Name",
),
keyboardType: TextInputType.text,
onChanged: (String value) {
_editedLastNameField = true;
widget.onLastNameChange(value);
},
validator: (String value) {
String error;
if (_editedLastNameField) {
error = value.isEmpty ? "This field is required" : null;
bool isValid = error == null;
widget.onLastNameValidationStatusUpdate(isValid);
}
return error;
},
),
],
),
);
}
#override
void initState() {
super.initState();
_firstName = new TextEditingController();
_lastName = new TextEditingController();
_editedFirstNameField = false;
_editedLastNameField = false;
}
#override
void dispose() {
super.dispose();
_firstName.dispose();
_lastName.dispose();
}
}
Then, in my widget that displays the form components, I do something like this.
class _SignUpFormState extends State<SignUpForm> {
String _firstName;
bool _firstNameIsValid;
String _lastName;
bool _lastNameIsValid;
int current;
final maxLength = 4;
final minLength = 0;
#override
Widget build(BuildContext context) {
// create the widget
return Container(
child: _showFormSection(),
);
}
/// Show the appropriate form section
Widget _showFormSection() {
Widget form;
switch (current) {
case 0:
form = _showNameFormSection();
break;
case 1:
form = _showEmailForm();
break;
case 2:
form = _showPasswordForm();
break;
case 3:
form = _showDobForm();
break;
}
return form;
}
// shows the name section of the form.
Widget _showNameFormSection() {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
child: SignUpNameForm(
firstNameInitialValue: _firstName,
lastNameInitialValue: _lastName,
onFirstNameChange: (String value) {
setState(() {
_firstName = value;
});
},
onFirstNameValidationStatusUpdate: (bool value) {
setState(() {
_firstNameIsValid = value;
});
},
onLastNameChange: (String value) {
setState(() {
_lastName = value;
});
},
onLastNameValidationStatusUpdate: (bool value) {
setState(() {
_lastNameIsValid = value;
});
},
),
),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
FlatButton(
child: Text("Next"),
onPressed: (_firstNameIsValid && _lastNameIsValid)
? _showNextSection
: null,
),
],
),
)
],
),
);
}
/// Shows the next section in the form
void _showNextSection() {
print("Current Section: " + current.toString());
setState(() {
if (current >= maxLength) {
current = maxLength;
} else {
current++;
}
});
print("Next Section: " + current.toString());
}
/// Show the previous section of the form
void _showPreviousSection() {
print("Current Section:" + current.toString());
setState(() {
if (current <= minLength) {
current = minLength;
} else {
current--;
}
print("Previous Section: " + current.toString());
});
}
#override
void initState() {
super.initState();
current = 0;
_firstName = "";
_firstNameIsValid = false;
_lastName = "";
_lastNameIsValid = false;
// other initializations
}
}
As you can see here, I pass in functions to extract the values of the user's name, as well as the status of the validation, and use that to determine whether or not I should enable the "next" button in the form handler widget. This is now causing a problem, specifically because the functions I pass into the "mini-form" invokes initState() while the widget is being rebuilt.
How might I go about handling this? Or, is there a better way I can go about implementing this multi-Page form that is cleaner?
Thanks.
build() should be fast and idempotent. You should not be calling setState() inside a build. Imagine build() is being called 60 times a second (although it won't be thanks to optimizations) and you'll have the proper mindset.