i am using stepper widget in order to collect info from user and validate it, i need to call an API at each step hence validate each field in a step at every continue button ... i am using form state and form widget but the issue is that it validates entire fields in all steps in stepper... how can i validate only individual step in a stepper? i went through the documentation in Stepper and State classes in stepper.dart but there is no supporting function there
following is the code
class SubmitPayment extends StatefulWidget {
SubmitPayment({Key key, this.identifier, this.amount, this.onResendPressed})
: super(key: key);
final String identifier;
final String amount;
final VoidCallback onResendPressed;
#override
State<StatefulWidget> createState() {
return _SubmitPaymentState();
}
}
class _SubmitPaymentState extends State<SubmitPayment> {
final GlobalKey<FormState> _formKeyOtp = GlobalKey<FormState>();
final FocusNode _otpFocusNode = FocusNode();
final TextEditingController _otpController = TextEditingController();
bool _isOTPRequired = false;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Form(
key: _formKeyOtp,
child: Column(children: <Widget>[
Center(
child: Padding(
padding:
EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0),
child: Text(
Translations.of(context).helpLabelOTP,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontStyle: FontStyle.italic),
))),
CustomTextField(
icon: Icons.vpn_key,
focusNode: _otpFocusNode,
hintText: Translations.of(context).otp,
labelText: Translations.of(context).otp,
controller: _otpController,
keyboardType: TextInputType.number,
hasError: _isOTPRequired,
validator: (String t) => _validateOTP(t),
maxLength: AppConstants.otpLength,
obscureText: true,
),
Center(
child: ButtonBar(
mainAxisSize: MainAxisSize.max,
alignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text(Translations.of(context).resendOtpButton),
color: Colors.white,
textColor: Theme.of(context).primaryColor,
onPressed: widget.onResendPressed,
),
RaisedButton(
child: Text(
Translations.of(context).payButton,
),
onPressed: _doPullPayment,
),
],
)),
])),
);
}
String _validateOTP(String value) {
if (value.isEmpty || value.length < AppConstants.otpLength) {
setState(() => _isOTPRequired = true);
return Translations.of(context).invalidOtp;
}
return "";
}
bool _validateOtpForm() {
_formKeyOtp.currentState.save();
return this._formKeyOtp.currentState.validate();
}
Future<void> _doPullPayment() async {
setState(() {
_isOTPRequired = false;
});
if (!_validateOtpForm()) return false;
try {
setState(() {
_isOTPRequired = false;
});
showDialog(
barrierDismissible: false,
context: context,
builder: (context) => AlertDialog(
content: ListTile(
leading: CircularProgressIndicator(),
title: Text(Translations.of(context).processingPaymentDialog),
),
),
);
TransactionApi api =
TransactionApi(httpDataSource, authenticator.sessionToken);
String responseMessage = await api.doPullPayment(
widget.identifier,
widget.amount,
_otpController.text,
TransactionConstants.transactionCurrency);
Navigator.of(context).pop();
await showAlertDialog(
context, Translations.of(context).pullPayment, '$responseMessage');
Navigator.pop(context);
} catch (exception) {
await showAlertDialog(context, Translations.of(context).pullPayment,
'${exception.message}');
Navigator.of(context).pop();
}
}
One approach is to use a separate Form for each step.
To handle that, use a list of GlobalKey<FormState> which you can index based on _currentStep, then call validate() in onStepContinue:
List<GlobalKey<FormState>> _formKeys = [GlobalKey<FormState>(), GlobalKey<FormState>(), …];
…
Stepper(
currentStep: _currentStep,
onStepContinue: () {
setState(() {
if (_formKeys[_currentStep].currentState?.validate()) {
_currentStep++;
}
});
},
steps:
Step(
child: Form(key: _formKeys[0], child: …),
This implies the following:
Since you're calling an API at the end, you need to check if you're validating the last step, and save instead of just validating;
You probably want to factor our the Forms to several widgets. If you do so, do not confuse the key parameter that every Widget has. Pass the formKey as an unnamed parameter to avoid confusion.
So i solved this as follows:
The problem was that i was returning an *empty string ("") * if the my logic was valid, where as validate method of FormState expects each validator method, associated with TextFormField to return null if validation is passed.
i changed following
String _validateOTP(String value) {
if (value.isEmpty || value.length < AppConstants.otpLength) {
setState(() => _isOTPRequired = true);
return Translations.of(context).invalidOtp;
}
return "";
}
to
String _validateOTP(String value) {
if (value.isEmpty || value.length < AppConstants.otpLength) {
setState(() => _isOTPRequired = true);
return Translations.of(context).invalidOtp;
}
return null;
}
and it worked all fine then.
Refer to this link for details
"If there is an error with the information the user has provided, the validator function must return a String containing an error message. If there are no errors, the function should not return anything."
It's been long since this question was asked. I hope my answer can help. To do this, I created a List<GlobalKey> then in the onContinue of the Stepper I did something as
final List<GlobalKey<FormState>> _formKeys = [
GlobalKey<FormState>(),
GlobalKey<FormState>(),
GlobalKey<FormState>(),
GlobalKey<FormState>()
]; continued() {
if(_formKeys[_currentStep].currentState!.validate()) {
switch(_currentStep){
case 0:
setSender();
break;
case 1:
setReceiver();
break;
}
}
}
Related
I have a form with some inputs. I am using a GlobalKey<FormState> to handle submissions and validation and so on.
One of the fields is supposed to take a double input, so I validate that by trying to parse the input value to double like so :
return TextFormField(
decoration: InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.number,
validator: (String value) {
double _parsedValue = double.tryParse(value);
if (_parsedValue == null) {
return "Please input a number";
}
},
onSaved: (String value) {
setState(() {
_price = double.parse(value);
});
},
);
Now that works as expected. However, if the user inputs for example 9,99 that would fail, because the parse expects 9.99 .
What I'm trying to do is, when the validator is called, I'd like to check the input string for any commas, and then if they are present, replace them with dots instead, and update the form value accordingly.
My question is - can we actually update the form state from within validators?
I think maybe what you need is a TextInputFormatter.
Here is a link to the docs https://docs.flutter.io/flutter/services/TextInputFormatter-class.html
There are pre-existing formatters you can use as a reference to convert comma's to dots.
I don't think you need to update the state in the validator. I would use only the save event to update the state. This way it gets very clear where the state is updated.
I believe nothing forbids you to update the state in the validate, but maybe it would get less organized. :)
Solution that do not exactly answer your question
I guess the best way to accomplish what you need would be using a TextInputFormatter with a WhitelistingTextInputFormatter, check it out:
Note the TextInputType.numberWithOptions(decimal: true) and that if the user pastes "-100,00" , it would become 100.0 - which for a price would be fine, but not for double values in general.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ValidatorState',
theme: ThemeData(primarySwatch: Colors.yellow),
home: MyFormPage(),
);
}
}
class MyFormPage extends StatefulWidget {
#override
_MyFormPageState createState() => _MyFormPageState();
}
class _MyFormPageState extends State<MyFormPage> {
final _formKey = GlobalKey<FormState>();
double _price;
void _save() {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
Scaffold.of(_formKey.currentContext)
.showSnackBar(SnackBar(content: Text('New price defined! ($_price)')));
}
}
Widget _buildForm(BuildContext context) {
return Container(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormField(
inputFormatters: <TextInputFormatter>[
WhitelistingTextInputFormatter(RegExp("[0-9.]"))
],
decoration: InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.numberWithOptions(decimal: true),
validator: (String value) {
double _parsedValue = double.tryParse(value);
if (_parsedValue == null) {
return "Please input a valid number";
}
if (_parsedValue == 0.0) {
return "Please input a valid price";
}
},
onSaved: (String value) {
setState(() {
_price = double.tryParse(value);
});
},
),
Text(""),
RaisedButton(
child: Text("Save"),
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).primaryTextTheme.title.color,
onPressed: _save,
),
Text(""),
TextFormField(
decoration: InputDecoration(labelText: 'Copy and Paste area'),
),
],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Validator State"),
),
body: Form(
key:_formKey,
child: _buildForm(context),
),
);
}
}
Solution that answers your question
However, that is not exactly what you described. You want to automatically replace , to .. I would avoid doing that, as 1,234.56 would translate to 1.234.56, which is invalid. If you only strip out the commas, you end up with 1234.56 which is valid.
If you really want to do as you said, you have to use a TextEditingController and a function to normalize the text data. I've made the example below, check it out - specially the _priceController and the _parsePrice.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ValidatorState',
theme: ThemeData(primarySwatch: Colors.yellow),
home: MyFormPage(),
);
}
}
class MyFormPage extends StatefulWidget {
#override
_MyFormPageState createState() => _MyFormPageState();
}
class _MyFormPageState extends State<MyFormPage> {
final _formKey = GlobalKey<FormState>();
TextEditingController _priceController;
double _price;
#override
void initState() {
super.initState();
_priceController = TextEditingController();
}
#override
void dispose() {
_priceController?.dispose();
super.dispose();
}
void _save() {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
Scaffold.of(_formKey.currentContext)
.showSnackBar(SnackBar(content: Text('New price defined! ($_price)')));
}
}
double _parsePrice(String text) {
var buffer = new StringBuffer();
text.runes.forEach((int rune) {
// acceptable runes are . or 0123456789
if (rune == 46 || (rune >= 48 && rune <= 57)) buffer.writeCharCode(rune);
// if we find a , we replace with a .
if (rune == 44) buffer.writeCharCode(46);
});
return double.tryParse(buffer.toString());
}
Widget _buildForm(BuildContext context) {
return Container(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormField(
controller: _priceController,
decoration: InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.numberWithOptions(decimal: true),
validator: (String value) {
double _parsedValue = _parsePrice(value);
if (_parsedValue == null) {
return "Please input a valid number";
}
if (_parsedValue == 0.0) {
return "Please input a valid price";
}
},
onSaved: (String value) {
setState(() {
_price = _parsePrice(value);
_priceController.text = _price.toString();
});
},
),
Text(""),
RaisedButton(
child: Text("Save"),
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).primaryTextTheme.title.color,
onPressed: _save,
),
],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Validator State"),
),
body: Form(
key:_formKey,
child: _buildForm(context),
),
);
}
}
hi did you get a fix for this?
I would rethink your strategy for this issue.
Maybe what you need is an observer function that is triggered when the user typing, which then looks at the comma and changes it to a dot.
TextFormField has a built in function,
onEditingCompleted and onFieldSubmitted which can run the function you have to make the check before the validate is run.
I am trying to use shared preference in my app with the bloc pattern.
Following is my code
class PrefsStats {
final bool isMale;
final String name;
final int age;
PrefsStats(this.isMale, this.name, this.age);
}
class PrefsBloc {
final _changePrefernce = BehaviorSubject<PrefsStats>();
Function(PrefsStats) get changePrefs => _changePrefernce.sink.add;
Stream<PrefsStats> get prefrence => _changePrefernce.stream;
SharedPreferences sPrefs;
dispose(){
_changePrefernce?.close();
}
PrefsBloc(){
_loadSharedPreferences();
}
Future<void> _loadSharedPreferences() async {
sPrefs = await SharedPreferences.getInstance();
final namePref = sPrefs.getString("name") ?? "";
final malePref = sPrefs.getBool("male") ?? false;
final agePref = sPrefs.getInt("age") ?? 0;
_changePrefernce.add(PrefsStats(malePref,namePref,agePref));
}
}
final prefsBloc = PrefsBloc();
I just want to insert data using one button and get data using another button from SharedPreferences
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
SizedBox(
height: 20,
),
RaisedButton(
onPressed: () {
prefsBloc.changePrefs(PrefsStats(true, "argo", 21));
},
child: Text("Insert Data"),
),
SizedBox(
height: 20,
),
RaisedButton(
onPressed: () {
prefsBloc.prefrence.forEach((data){
print(data.name);
});
},
child: Text("Get Data"),
),
SizedBox(
height: 20,
),
],
)),
),
);
}
#override
void dispose() {
prefsBloc?.dispose();
super.dispose();
}
}
Whenever I close my app and reopen it again and I click get data button at the start even before inserting data, I get default values. I know I am not assigning keys at the time of setting value, which is causing the confusion of how to use shared preferences with bloc. And the other problem is whenever I set data, the code inside get data button gets called even before pressing get data which I fail to understand.
There exits two places on your code that must be fixed.
First of all, in your BloC class, your stream must Listen whenever a sink is added,
.
.
.
PrefsBloc(){
_loadSharedPreferences();
_changePrefernce.stream.listen(_newFunction);
}
void _newFunction(PrefsStats stats){
if (states != null) {
if (sPrefs != null) {
sPrefs.setString("name", states.name);
sPrefs.setInt("age", states.age);
sPrefs.setBool("male", states.isMale);
sPrefs.commit();
}
}
}
Second place is in _MyAppState class, in the build function you have to wrap Scaffold with a StreamBuilder,
class _MyHomePageState extends State<MyHomePage> {
String textAge = "";
#override
Widget build(BuildContext context) {
return MaterialApp(
home: StreamBuilder(
stream: prefsBloc.prefrence,
builder: (context, AsyncSnapshot<PrefsStats> snapshot) {
return Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
Text((snapshot.data != null) ? snapshot.data.name : ""),
SizedBox(
height: 20,
),
RaisedButton(
onPressed: () {
prefsBloc.changePrefs(PrefsStats(
true,
textAge.toString(),
21,
));
},
child: Text("Insert Data"),
),
TextFormField(
initialValue: (snapshot.data != null) ? snapshot.data.name : "",
onFieldSubmitted: (value) {
textAge = value;
},
),
Text(textAge),
SizedBox(
height: 20,
),
RaisedButton(
onPressed: () {
prefsBloc.prefrence.forEach((data) {
print(data.name);
setState(() {
textAge = data.name;
});
});
},
child: Text("Get Data"),
),
SizedBox(
height: 20,
),
],
)),
);
},
));
}
I am new to flutter. in my project, there is a various check_list_tile depending upon the length of the List (attendance list). And I have used one Boolean variable. Now when I press on one checkbox it automatically checks all other checkboxes. Please help me in this (on tap one checkbox should not change the state of all other checkboxes except clicked). I have copied all code please check check_box_list field.
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'dart:async';
Map map_student_data;
Iterable iter_student_data,iter_student_key;
List list_student_data,list_student_key;
bool t=true,checkbox=false;
List list;
String validation="yes";
int i;
int year;
final FirebaseDatabase database = FirebaseDatabase.instance;
class IImca_attendence extends StatefulWidget {
#override
_IImca_attendenceState createState() => _IImca_attendenceState();
}
class _IImca_attendenceState extends State<IImca_attendence> {
#override
void initState(){
this.check_year();
super.initState();
}
DateTime date = DateTime.now();
Future check_year()async{
var k= await database.reference().child("NITTE/CLASS/MCA").once().then((DataSnapshot snapshot){
Map sea= snapshot.value;
Iterable iter=sea.keys;
list=iter.toList();
list.sublist(list.length-1);
list.sort();
setState(() {
year=list.length-2;
});
check();
});
}
Future check()async{
var m=await database.reference().child("NITTE/CLASS/MCA/${list[year].toString().toUpperCase()}/STUDENT").once().then((DataSnapshot currentyear){
map_student_data=currentyear.value;
iter_student_data=map_student_data.values;
iter_student_key=map_student_data.keys;
list_student_data=iter_student_data.toList();
list_student_key=iter_student_key.toList();
for(i=0;i<=list_student_data.length;i++){
bool ss=true;
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("II-MCA"),
centerTitle: true,
actions: <Widget>[
IconButton(icon: Icon(Icons.refresh),onPressed: (){
setState(() {
check_year();
});
})
],
),
body:
validation=="1"?
new Center(
child: Text("STUDENT DOSE NOT EXIST IN $year",style: TextStyle(color: Colors.grey,fontWeight: FontWeight.bold,fontSize: 20),),
):
new ListView.builder(
itemCount: list_student_data==null?0
:list_student_data.length,
itemBuilder: (BuildContext context,int index){
var student_detail= ['NAME : ${list_student_data[index]['NAME']}','GENDER : ${list_student_data[index]['CURRENT CLASS']}','PHOTO : ${list_student_data[index]['PHOTO']}'];
return new Container(
child: new Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Card(
child: new Container(
child: CheckboxListTile(
title: Text("${list_student_key[index]}",style:TextStyle(fontWeight: FontWeight.bold,),),
subtitle: Text("NAME : ${list_student_data[index]['NAME']}"),
value: checkbox,
onChanged: (val){
setState(() {
checkbox=val;
if(checkbox==true){
print("${list_student_data[index]['NAME']}: i am absent");
}if(checkbox==false){
print("${list_student_data[index]['NAME']}: i am present");
}
});
},
),
padding: EdgeInsets.all(5),
),
)
],
),
),
);
}
)
);
}
}
Well... you are using a global checkbox variable, so It's quite normal that if you change it, all widget depending on its state will change accordingly.
What I suggest you to do is to add the selected state inside your model class. Just as an example, assumed you have this Student class (I know you are using firebase, but for sake of time I don't)
class Student {
var name = 'foo';
var year = '2018';
var selected = false;
Student(this.name);
}
This class has is selected state inside of it.
Now assume that your snapshot give you 3 students. Always for sake of time I've embedded a local array:
class _IImca_attendenceState extends State<IImca_attendence> {
var _students = [Student('foo'), Student('pub'), Student('beer')];
...
(Ellipses are not part of code... ;-])
I suggest you to put your state variables inside the Stateful Widget scope and not onto the Global Scope.
That said you could have:
ListView.builder(
itemCount: _students.length,
itemBuilder: (BuildContext context, int index) {
return new Container(
child: new Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Card(
child: new Container(
child: CheckboxListTile(
title: Text(
_students[index].name,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle:
Text("NAME : ${_students[index].name}"),
value: _students[index].selected,
onChanged: (val) {
setState(() {
_students[index].selected = val;
if (!_students[index].selected) {
print(
'${_students[index].name}: i am absent');
}
if (_students[index].selected) {
print(
'${_students[index].name}: i am present');
}
});
},
),
padding: EdgeInsets.all(5),
),
)
],
),
),
);
}
)
You should also use an array of bool(s) of the same length of your snapshot data students array... but I'd like to suggest to track this information directly on your Student model.
UPDATE
As you are more comfortable using array I've change my code using a complementary array of bool of the same size of your student list.
All you have to do is an array (not a single value) of boolean values, the same size of your student array, lets call this list_student_present (I instead use list_student_present2)
At the beginning you initialize this in your check function a way like that:
list_student_data = iter_student_data.toList();
// This is the array you wanna use (first all false)
list_student_present = iter_student_data.map((_) => false).toList();
And then you will use this array of bool to check the state of your checkboxes:
itemBuilder: (BuildContext context, int index) {
return new Container(
child: new Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Card(
child: new Container(
child: CheckboxListTile(
title: Text(
_students[index].name,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle:
Text("NAME : ${_students[index].name}"),
value: list_student_present[index],
onChanged: (val) {
setState(() {
list_student_present[index] = val;
if (!list_student_present[index]) {
print(
'${_students[index].name}: i am absent');
}
if (list_student_present[index]) {
print(
'${_students[index].name}: i am present');
}
});
},
),
padding: EdgeInsets.all(5),
),
)
],
),
),
);
}
Do not use my Student class (I continue using it so that I'm able to show you data without firebase), continue using your students array list from your firebase snapshot.
Full code:
import 'package:flutter/material.dart';
import 'dart:async';
Map map_student_data;
Iterable iter_student_data, iter_student_key;
List list_student_data, list_student_key, list_student_present, list_student_present2;
bool t = true;
List list;
String validation = "yes";
int i;
int year;
class Student {
var name = 'foo';
var year = '2018';
var selected = false;
Student(this.name);
}
class IImca_attendence extends StatefulWidget {
#override
_IImca_attendenceState createState() => _IImca_attendenceState();
}
class _IImca_attendenceState extends State<IImca_attendence> {
var _students = [Student('foo'), Student('pub'), Student('beer')];
#override
void initState() {
this.check_year();
super.initState();
}
DateTime date = DateTime.now();
Future check_year() async {
Map sea = {1: 'atlantic', 2: 'pacific'};
Iterable iter = sea.keys;
list = iter.toList();
list.sublist(list.length - 1);
list.sort();
setState(() {
year = list.length - 2;
});
check();
}
Future check() async {
map_student_data = {
0: {'NAME': 'foo', 'CURRENT CLASS': 'pub', 'PHOTO': ''}
};
iter_student_data = map_student_data.values;
iter_student_key = map_student_data.keys;
list_student_data = iter_student_data.toList();
// This is the array you wanna use
list_student_present = iter_student_data.map((_) => false).toList();
// This is the array for my example
list_student_present2 = _students.map((_) => false).toList();
list_student_key = iter_student_key.toList();
for (i = 0; i <= list_student_data.length; i++) {
bool ss = true;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("II-MCA"),
centerTitle: true,
actions: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
setState(() {
check_year();
});
})
],
),
body: validation == "1"
? new Center(
child: Text(
"STUDENT DOSE NOT EXIST IN $year",
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
fontSize: 20),
),
)
: ListView.builder(
itemCount: _students.length,
itemBuilder: (BuildContext context, int index) {
return new Container(
child: new Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Card(
child: new Container(
child: CheckboxListTile(
title: Text(
_students[index].name,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle:
Text("NAME : ${_students[index].name}"),
value: list_student_present2[index],
onChanged: (val) {
setState(() {
list_student_present2[index] = val;
if (!list_student_present2[index]) {
print(
'${_students[index].name}: i am absent');
}
if (list_student_present2[index]) {
print(
'${_students[index].name}: i am present');
}
});
},
),
padding: EdgeInsets.all(5),
),
)
],
),
),
);
}));
}
}
I really don't like this solution. What I suggest you is to create your PODOs (Plain Old Dart Object) representing your firebase models and deserialise them from your firebase snapshots.
I'm trying to create a preferences menu where I have three settings (e. g. 'notifications') stored with Shared Preferences. They are applied to SwitchListTiles.
Everytime my settings tab is selected there is an error (I/flutter (22754): Another exception was thrown: 'package:flutter/src/material/switch_list_tile.dart': Failed assertion: line 84 pos 15: 'value != null': is not true.) appearing just a millisecond. After that the correct settings are displayed. This happens when I don't add a default value to the variables initialized in 'ProfileState'. If they have a default value the error disappears but the switches are 'flickering' at tab selection from the default value to the correct value in Shared Preferences.
My assumption is that my loadSettings function is executed after the build method.
How can I solve that? Any help is appreciated.
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class Profile extends StatefulWidget {
#override
ProfileState createState() {
return new ProfileState();
}
}
class ProfileState extends State<Profile> {
bool notifications;
bool trackHistory;
bool instantOrders;
#override
void initState() {
super.initState();
loadSettings();
}
//load settings
loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
notifications = (prefs.getBool('notifications') ?? true);
trackHistory = (prefs.getBool('trackHistory') ?? true);
instantOrders = (prefs.getBool('instantOrders') ?? false);
});
}
//set settings
setSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool('notifications', notifications);
prefs.setBool('trackHistory', trackHistory);
prefs.setBool('instantOrders', instantOrders);
}
#override
Widget build(BuildContext context) {
return new ListView(
children: <Widget>[
new Row(
children: <Widget>[
new Container(
padding: new EdgeInsets.fromLTRB(20.0, 20.0, 0.0, 8.0),
child: new Text("General", style: new TextStyle(color: Colors.black54)),
)
],
),
new SwitchListTile(
title: const Text('Receive Notifications'),
activeColor: Colors.brown,
value: notifications,
onChanged: (bool value) {
setState(() {
notifications = value;
setSettings();
});
},
secondary: const Icon(Icons.notifications, color: Colors.brown),
),
new SwitchListTile(
title: const Text('Track History of Orders'),
activeColor: Colors.brown,
value: trackHistory,
onChanged: (bool value) {
setState((){
trackHistory = value;
setSettings();
});
},
secondary: const Icon(Icons.history, color: Colors.brown,),
),
new SwitchListTile(
title: const Text('Force instant Orders'),
activeColor: Colors.brown,
value: instantOrders,
onChanged: (bool value) {
setState((){
instantOrders = value;
setSettings();
});
},
secondary: const Icon(Icons.fast_forward, color: Colors.brown),
),
new Divider(
height: 10.0,
),
new Container(
padding: EdgeInsets.all(32.0),
child: new Center(
child: new Column(
children: <Widget>[
new TextField(
)
],
),
),
),
new Divider(
height: 10.0,
),
new Row(
children: <Widget>[
new Container(
padding: new EdgeInsets.fromLTRB(20.0, 20.0, 0.0, 20.0),
child: new Text("License Information", style: new TextStyle(color: Colors.black54)),
)
],
),
new Container(
padding: new EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 20.0) ,
child: new RichText(
text: new TextSpan(
text: "With confirming our terms and conditions you accept full usage of your personal data. Yikes!",
style: new TextStyle(color: Colors.black)
)
)
)
]
);
}
}
EDIT
I tried to solve it with the recommended FutureBuilder from Darek's solution. The initial error is solved now but now I face another inconvenience. The tab builds itself completely everytime a switch is tapped which is clearly noticable. Furthermore the switches don't run smoothly anymore. On startup of the app you can also see the waiting message shortly which isn't that pretty.
Here is the new class in the code:
class ProfileState extends State<Profile> {
bool notifications;
bool trackHistory;
bool instantOrders;
SharedPreferences prefs;
#override
void initState() {
super.initState();
loadSettings();
}
//load settings
Future<String> loadSettings() async {
prefs = await SharedPreferences.getInstance();
notifications= (prefs.getBool('notifications') ?? true);
trackHistory = (prefs.getBool('trackHistory') ?? true);
instantOrders= (prefs.getBool('instantOrders') ?? true);
}
//set settings
setSettings() async {
prefs.setBool('notifications', notifications);
prefs.setBool('trackHistory', trackHistory);
prefs.setBool('instantOrders', instantOrders);
}
#override
Widget build(BuildContext context) {
var profileBuilder = new FutureBuilder(
future: loadSettings(), // a Future<String> or null
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('No preferences');
case ConnectionState.waiting:
return new Text('Loading preferences');
case ConnectionState.done:
if (snapshot.hasError)
return new Text('Error: ');
else
return new Column(
children: <Widget>[
new Row(
children: <Widget>[
new Container(
padding: new EdgeInsets.fromLTRB(20.0, 20.0, 0.0, 8.0),
child: new Text("General", style: new TextStyle(color: Colors.black54)),
)
],
),
new SwitchListTile(
title: const Text('Receive Notifications'),
activeColor: Colors.brown,
value: notifications,
onChanged: (bool value) {
setState(() {
notifications = value;
setSettings();
});
},
secondary: const Icon(Icons.notifications, color: Colors.brown),
),
new SwitchListTile(
title: const Text('Track History of Orders'),
activeColor: Colors.brown,
value: trackHistory,
onChanged: (bool value) {
setState((){
trackHistory = value;
setSettings();
});
},
secondary: const Icon(Icons.history, color: Colors.brown,),
),
new SwitchListTile(
title: const Text('Force instant Orders'),
activeColor: Colors.brown,
value: instantOrders,
onChanged: (bool value) {
setState((){
instantOrders = value;
setSettings();
});
},
secondary: const Icon(Icons.fast_forward, color: Colors.brown),
),
new Divider(
height: 10.0,
),
new Row(
children: <Widget>[
new Container(
padding: new EdgeInsets.fromLTRB(20.0, 20.0, 0.0, 20.0),
child: new Text("License Information", style: new TextStyle(color: Colors.black54)),
)
],
),
new Container(
padding: new EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 20.0) ,
child: new RichText(
text: new TextSpan(
text: "With confirming our terms and conditions you accept full usage of your personal data. Yikes!",
style: new TextStyle(color: Colors.black)
)
)
)
]
);
}
},
);
return new Scaffold(
body: profileBuilder,
);
}
}
The lifecycle of a State object goes createState -> initState -> didChangeDependencies -> build (see the linked doc for more details). So in your case it's not an ordering problem. What's actually happening is that loadSettings is getting called, but as soon as it hits the await a Future is return and execution of the caller continues (see async/await in the Dart docs). So, your widget tree is being built and your initially null values are being used, then the async part gets executed and your variables are initialised and setState is called triggering the rebuild, which works fine.
What you need to use is a FutureBuilder so that you can build the UI accordingly when the Future has finished:
new FutureBuilder(
future: _calculation, // a Future<String> or null
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none: return new Text('Press button to start');
case ConnectionState.waiting: return new Text('Awaiting result...');
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return new Text('Result: ${snapshot.data}');
}
},
)
In the above example, you'd replace _calculation with loadSettings and return the relevant UIs in the none and waiting states (the latter will be your one with the SwitchListTiles).
To fix the problem of your Edit, store the Future from the loadSettings call in your initState and use this Future for the Future Builder. What you are doing now is calling the function loadSettings everytime your UI rebuilds.
class ProfileState extends State<Profile> {
bool notifications;
bool trackHistory;
bool instantOrders;
SharedPreferences prefs;
Future<String> loadSettingsFuture; // <-Add this
#override
void initState() {
super.initState();
loadSettingsFuture = loadSettings();// <- Change This
}
...
...
...
#override
Widget build(BuildContext context) {
var profileBuilder = new FutureBuilder(
future: loadSettingsFuture, // <-- Change this
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
...
...
I am trying to pass data from one screen to another, but I keep getting a null exception. Whenever I fill in the form on the first screen and proceed to next screen, I get a `
NoSuchMethodError: The getter 'storeNumber' was called on null
`
My variables class is ==> This entity class has variables that I populate using a form in the following class:
class StoreData {
String _storeNumber;
String _repName;
String _repCell;
DateTime _transactionDate = new DateTime.now();
StoreData(
this._storeNumber, this._repName, this._repCell, this._transactionDate);
String get storeNumber => _storeNumber;
set storeNumber(String value) {
_storeNumber = value;
}
String get repName => _repName;
DateTime get transactionDate => _transactionDate;
set transactionDate(DateTime value) {
_transactionDate = value;
}
String get repCell => _repCell;
set repCell(String value) {
_repCell = value;
}
set repName(String value) {
_repName = value;
}
}
The main class (in this case this is the first screen that sends data to second screen) includes the following code:
This class has a form that takes in 3 inputs and send them to second screen.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'FeedBack.dart';
import 'StoreData.dart';
void main() {
runApp(MaterialApp(
title: 'Navigation Basics',
home: FirstScreen(),
));
}
//get our entity class
StoreData storeDate;
// get variables from entity class
String storeNumber = storeDate.storeNumber;
String repName = storeDate.repName;
String repCell = storeDate.repCell;
DateTime transactionDate = storeDate.transactionDate;
class FirstScreen extends StatefulWidget {
#override
_FirstScreenState createState() => _FirstScreenState();
}
class _FirstScreenState extends State<FirstScreen> {
GlobalKey<FormState> _key = GlobalKey();
bool _validate = false;
_sendData() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FeedBack(
storeData: new StoreData(
storeNumber, repName, repCell, transactionDate))),
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text('Test App'),
),
body: new SingleChildScrollView(
child: new Container(
margin: new EdgeInsets.all(15.0),
child: new Form(
key: _key,
autovalidate: _validate,
child: formUI(),
),
),
),
),
);
}
Widget formUI() {
return new Column(
children: <Widget>[
new TextFormField(
decoration: new InputDecoration(hintText: 'Store Number'),
keyboardType: TextInputType.number,
validator: validateRepCell,
onSaved: (String val) {
storeNumber = val;
}),
new TextFormField(
decoration: new InputDecoration(hintText: 'Rep Full Name'),
validator: validateRepName,
onSaved: (String val) {
repName = val;
}),
new TextFormField(
decoration: new InputDecoration(hintText: 'Rep Phone Number'),
keyboardType: TextInputType.number,
validator: validateRepCell,
onSaved: (String val) {
repCell = val;
}),
new SizedBox(height: 15.0),
new RaisedButton(
onPressed: _sendData,
child: new Text('Proceed'),
)
],
);
}
// Validate Fields
String validateRepCell(String value) {
// String patttern = r'(^[a-zA-Z ]*$)';
RegExp regExp = new RegExp(r'^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)$');
if (value.length == 0) {
return "Store Number is Required";
} else if (!regExp.hasMatch(value)) {
return "Store Number must be only have numbers";
}
return null;
}
String validateRepName(String value) {
String patttern = r'(^[a-zA-Z ]*$)';
RegExp regExp = new RegExp(patttern);
if (value.length == 0) {
return "Rep Name is Required";
} else if (!regExp.hasMatch(value)) {
return "Name must be a-z and A-Z";
}
return null;
}
}
The second screen's code is here:
class FeedBack extends StatelessWidget {
final StoreData storeData;
FeedBack({Key key, #required this.storeData}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("FeedBack Screen"),
),
body: new Container(
child: new Column(
children: <Widget>[
new RaisedButton(
onPressed: _sendToDatabase,
child: new Text('Press Me'),
),
new Text("${storeData.storeNumber}"),
],
),
),
);
}
_sendToDatabase() {
Firestore.instance.runTransaction((Transaction transaction) async {
CollectionReference reference = Firestore.instance.collection('Stores');
await reference.add({"test": "test", "testII": "test"});
});
}
}
I have been trying to solve this problem for a week now, but given my new experience with Dart and Flutter framework, it has been tough !
Any help would be appreciated,
You can use the following approach.
Remove the following lines from your code:
//get our entity class
StoreData storeDate;
As initially there will be no instance of StoreData available right now.
Now, declare new variables like the following:
String storeNumber;
String repName;
String repCell;
DateTime transactionDate;
And then assign the form values to them in onSaved method.
So when your form will be submitted, these values will be used for creating new StoreData and it will be passed to the Second page.
Here is the code for your main.dart file:
import 'package:flutter/material.dart';
import 'FeedBack.dart';
import 'StoreData.dart';
void main() {
runApp(MaterialApp(
title: 'Navigation Basics',
home: FirstScreen(),
));
}
// get variables from entity class
String storeNumber;
String repName;
String repCell;
DateTime transactionDate = DateTime.now();
class FirstScreen extends StatefulWidget {
#override
_FirstScreenState createState() => _FirstScreenState();
}
class _FirstScreenState extends State<FirstScreen> {
GlobalKey<FormState> _key = GlobalKey();
bool _validate = false;
_sendData() {
_key.currentState.save();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FeedBack(
storeData: StoreData(
storeNumber, repName, repCell, transactionDate))),
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text('Test App'),
),
body: new SingleChildScrollView(
child: new Container(
margin: new EdgeInsets.all(15.0),
child: new Form(
key: _key,
autovalidate: _validate,
child: formUI(),
),
),
),
),
);
}
Widget formUI() {
return new Column(
children: <Widget>[
new TextFormField(
decoration: new InputDecoration(hintText: 'Store Number'),
keyboardType: TextInputType.number,
validator: validateRepCell,
onSaved: (String val) {
storeNumber = val;
}),
new TextFormField(
decoration: new InputDecoration(hintText: 'Rep Full Name'),
validator: validateRepName,
onSaved: (String val) {
repName = val;
}),
new TextFormField(
decoration: new InputDecoration(hintText: 'Rep Phone Number'),
keyboardType: TextInputType.number,
validator: validateRepCell,
onSaved: (String val) {
repCell = val;
}),
new SizedBox(height: 15.0),
new RaisedButton(
onPressed: _sendData,
child: new Text('Proceed'),
)
],
);
}
// Validate Fields
String validateRepCell(String value) {
// String patttern = r'(^[a-zA-Z ]*$)';
RegExp regExp = new RegExp(r'^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)$');
if (value.length == 0) {
return "Store Number is Required";
} else if (!regExp.hasMatch(value)) {
return "Store Number must be only have numbers";
}
return null;
}
String validateRepName(String value) {
String patttern = r'(^[a-zA-Z ]*$)';
RegExp regExp = new RegExp(patttern);
if (value.length == 0) {
return "Rep Name is Required";
} else if (!regExp.hasMatch(value)) {
return "Name must be a-z and A-Z";
}
return null;
}
}