How to properly use GetX with TextField and some server side validation? - flutter

I'm using get package for my project's state management. But i'm confused by the implementation for form inputs and validation since i couldn't find any example for it in the documentation. I have some questions regarding to this issue.
Should i create separate controller for each action? like PageOneController and PageOneFormController?
How to prevent the parent from rebuilding the child when the state changes?
How to trigger rebuild on data addition inside the Model().obs object?
As I mentioned above at point number 1, i found that using separate controller is a bit repetitive and unnecessary, but using same controller at multiple places prevents me to reset the state when i leave the child page since the controller was only destroyed when I leave the page which has initialized the state. To prevent any confusion from my explanation, please have a look at the illustration below.
At point number 2, as we know that the TextField widget accepts errorText for displaying an error message which is only accept a string. With this package in mind, when I try to change state of the error by the onChanged: (value) {} event, it rebuilds the entire widget everytime i type a value inside it which causing the input indicator stayed at the beginning.
In this case, point number 2 is no more happened but now it won't update the error state at all and it keep showing the error message, even when i typed a new value onto it.
Please help, here is my script:
education_info_create_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:prismahrfinal/app/controllers/account_info/education_info_controller.dart';
import 'package:prismahrfinal/app/ui/widgets/form_input.dart';
class EducationInfoCreatePage extends GetWidget<EducationInfoController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: Text('Add Education Info'),
floating: true,
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Column(
children: <Widget>[
Obx(
() => FormInput(
autofocus: true,
label: 'Institution name',
focusNode: controller.institutionFN,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
errorText: controller.institutionError,
onChanged: (value) {
controller.institutionError = null;
controller.institution = value;
},
onSubmitted: (_) {
controller.graduationMonthFN.requestFocus();
},
),
),
Obx(
() => FormInput(
label: 'Graduation month',
focusNode: controller.graduationMonthFN,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
errorText: controller.graduationMonthError,
onChanged: (value) {
controller.graduationMonthError = null;
controller.graduationMonth = int.parse(value);
},
onSubmitted: (_) {
controller.graduationYearFN.requestFocus();
},
),
),
Obx(
() => FormInput(
label: 'Graduation Year',
focusNode: controller.graduationYearFN,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
errorText: controller.graduationYearError,
onChanged: (value) {
controller.graduationYearError = null;
controller.graduationYear = int.parse(value);
},
onSubmitted: (_) {
controller.qualificationFN.requestFocus();
},
),
),
Obx(
() => FormInput(
label: 'Qualification',
focusNode: controller.qualificationFN,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
errorText: controller.qualificationError,
onChanged: (value) {
controller.qualificationError = null;
controller.qualification = value;
},
onSubmitted: (_) {
controller.locationFN.requestFocus();
},
),
),
Obx(
() => FormInput(
label: 'Location',
focusNode: controller.locationFN,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
errorText: controller.locationError,
onChanged: (value) {
controller.locationError = null;
controller.location = value;
},
onSubmitted: (_) {
controller.fieldOfStudyFN.requestFocus();
},
),
),
Obx(
() => FormInput(
label: 'Field of study',
focusNode: controller.fieldOfStudyFN,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
errorText: controller.fieldOfStudyError,
onChanged: (value) {
controller.fieldOfStudyError = null;
controller.fieldOfStudy = value;
},
onSubmitted: (_) {
controller.majorsFN.requestFocus();
},
),
),
Obx(
() => FormInput(
label: 'Majors',
focusNode: controller.majorsFN,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
errorText: controller.majorsError,
onChanged: (value) {
controller.majorsError = null;
controller.majors = value;
},
onSubmitted: (_) {
controller.finalScoreFN.requestFocus();
},
),
),
Obx(
() => FormInput(
label: 'Final Score',
focusNode: controller.finalScoreFN,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
errorText: controller.finalScoreError,
onChanged: (value) {
controller.finalScoreError = null;
controller.finalScore = double.parse(value);
},
onSubmitted: (_) {
controller.additionalInfoFN.requestFocus();
},
),
),
Obx(
() => FormInput(
label: 'Additional Info (optional)',
focusNode: controller.additionalInfoFN,
keyboardType: TextInputType.multiline,
maxLines: 5,
textInputAction: TextInputAction.go,
errorText: controller.additionalInfoError,
onChanged: (value) {
controller.additionalInfoError = null;
controller.additionalInfo = value;
},
onSubmitted: (_) {
controller.add();
},
),
),
],
),
),
),
],
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.send, color: Colors.white),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: Theme.of(context).primaryColor,
tooltip: 'Add Education Info',
onPressed: controller.add,
),
);
}
}
education_info_controller.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:meta/meta.dart';
import 'package:pretty_json/pretty_json.dart';
import 'package:prismahrfinal/app/data/models/account_info/education_info.dart';
import 'package:prismahrfinal/app/data/models/account_info/education_info_error.dart';
import 'package:prismahrfinal/app/data/repositories/account_info/education_info_repository.dart';
class EducationInfoController extends GetxController {
EducationInfoController({#required this.repository})
: assert(repository != null);
final EducationInfoRepository repository;
final FocusNode _institutionFN = FocusNode();
final FocusNode _graduationMonthFN = FocusNode();
final FocusNode _graduationYearFN = FocusNode();
final FocusNode _qualificationFN = FocusNode();
final FocusNode _locationFN = FocusNode();
final FocusNode _fieldOfStudyFN = FocusNode();
final FocusNode _majorsFN = FocusNode();
final FocusNode _finalScoreFN = FocusNode();
final FocusNode _additionalInfoFN = FocusNode();
final Rx<ListEducationInfo> data = ListEducationInfo().obs;
final Rx<EducationInfo> education = EducationInfo().obs;
final Rx<EducationInfoError> errors = EducationInfoError().obs;
#override
void onInit() => fetchDataFromApi();
void fetchDataFromApi() async {
data.value = await repository.getData();
if (data.value == null) {
Get.snackbar("Error", "Can't connect to server");
}
}
void add() async {
this._unfocus();
debugPrint(prettyJson(education.value.toJson()));
final response = await repository.add(education.value.toJson());
if (response == null) {
Get.snackbar("Error", "Can't connect to server");
return;
} else if (response is EducationInfoError) {
errors.value = response;
return;
}
data.value.educations.add(response);
}
void _unfocus() {
this.institutionFN.unfocus();
this.graduationMonthFN.unfocus();
this.graduationYearFN.unfocus();
this.qualificationFN.unfocus();
this.locationFN.unfocus();
this.fieldOfStudyFN.unfocus();
this.majorsFN.unfocus();
this.finalScoreFN.unfocus();
this.additionalInfoFN.unfocus();
}
// Getters -- Focus Nodes
get institutionFN => this._institutionFN;
get graduationMonthFN => this._graduationMonthFN;
get graduationYearFN => this._graduationYearFN;
get qualificationFN => this._qualificationFN;
get locationFN => this._locationFN;
get fieldOfStudyFN => this._fieldOfStudyFN;
get majorsFN => this._majorsFN;
get finalScoreFN => this._finalScoreFN;
get additionalInfoFN => this._additionalInfoFN;
// Getters -- Values
get institution => this.education.value.institution;
get graduationMonth => this.education.value.graduationMonth;
get graduationYear => this.education.value.graduationYear;
get qualification => this.education.value.qualification;
get location => this.education.value.location;
get fieldOfStudy => this.education.value.fieldOfStudy;
get majors => this.education.value.majors;
get finalScore => this.education.value.finalScore;
get additionalInfo => this.education.value.additionalInfo;
// Getters -- Errors
get institutionError => this.errors.value.institution?.first;
get graduationMonthError => this.errors.value.graduationMonth?.first;
get graduationYearError => this.errors.value.graduationYear?.first;
get qualificationError => this.errors.value.qualification?.first;
get locationError => this.errors.value.location?.first;
get fieldOfStudyError => this.errors.value.fieldOfStudy?.first;
get majorsError => this.errors.value.majors?.first;
get finalScoreError => this.errors.value.finalScore?.first;
get additionalInfoError => this.errors.value.additionalInfo?.first;
// Setters -- Values
set institution(value) => this.education.value.institution = value;
set graduationMonth(value) => this.education.value.graduationMonth = value;
set graduationYear(value) => this.education.value.graduationYear = value;
set qualification(value) => this.education.value.qualification = value;
set location(value) => this.education.value.location = value;
set fieldOfStudy(value) => this.education.value.fieldOfStudy = value;
set majors(value) => this.education.value.majors = value;
set finalScore(value) => this.education.value.finalScore = value;
set additionalInfo(value) => this.education.value.additionalInfo = value;
// Setters -- Errors
set institutionError(value) => this.errors.value.institution = value;
set graduationMonthError(value) => this.errors.value.graduationMonth = value;
set graduationYearError(value) => this.errors.value.graduationYear = value;
set qualificationError(value) => this.errors.value.qualification = value;
set locationError(value) => this.errors.value.location = value;
set fieldOfStudyError(value) => this.errors.value.fieldOfStudy = value;
set majorsError(value) => this.errors.value.majors = value;
set finalScoreError(value) => this.errors.value.finalScore = value;
set additionalInfoError(value) => this.errors.value.additionalInfo = value;
}
app_pages.dart
import 'package:get/get.dart';
import 'package:prismahrfinal/app/bindings/education_info_binding.dart';
import 'package:prismahrfinal/app/bindings/employment_info_binding.dart';
import 'package:prismahrfinal/app/bindings/personal_info_binding.dart';
import 'package:prismahrfinal/app/ui/android/account_info/education_info/education_info.dart';
import 'package:prismahrfinal/app/ui/android/account_info/education_info/education_info_create.dart';
import 'package:prismahrfinal/app/ui/android/account_info/employment_info/employment_info.dart';
import 'package:prismahrfinal/app/ui/android/account_info/personal_info/personal_info.dart';
import 'package:prismahrfinal/app/ui/android/account_info/personal_info/personal_info_edit.dart';
import 'package:prismahrfinal/app/ui/android/home.dart';
import 'package:prismahrfinal/app/ui/android/login.dart';
import 'package:prismahrfinal/app/ui/android/account_info.dart';
part './app_routes.dart';
abstract class AppPages {
static final pages = [
GetPage(
name: Routes.EDUCATION_INFO,
page: () => EducationInfoPage(),
binding: EducationInfoBinding(),
),
GetPage(
name: Routes.EDUCATION_INFO_CREATE,
page: () => EducationInfoCreatePage(),
),
];
}

Problem 1 - In my opnion each screen needs one controller. So every screen you are creating you need to create a controller too. If you need pass data between screen you need use Get.arguments to catch the arguments between your routes. To pass you will need just pass lol.
Get.toNamed(yourRoute, arguments: yourArgument);
Problem 2 - Every time you are updating the list errors all yours observers will observer the update.
// Getters -- Errors
get institutionError => this.errors.value.institution?.first;
get graduationMonthError => this.errors.value.graduationMonth?.first;
get graduationYearError => this.errors.value.graduationYear?.first;
get qualificationError => this.errors.value.qualification?.first;
get locationError => this.errors.value.location?.first;
get fieldOfStudyError => this.errors.value.fieldOfStudy?.first;
get majorsError => this.errors.value.majors?.first;
get finalScoreError => this.errors.value.finalScore?.first;
get additionalInfoError => this.errors.value.additionalInfo?.first;
thats the reason all widgets are rebuilding....
Problem 3 - You can use the update method to trigger the react on your model.
use model.update((model){
model.email = 'foo#bar'
});

Related

Flutter Checkbox value storage in Firebase

My setup is simple: i am making a landing page using Flutter web: users input their email add and tick a Checkbox if say they are over 18.
For the life of me I can not find a way to store both email add and the boolean value of the checkbox in the SAME RECORD in Firebase?
the UI and Firebase all setup and working ok, here the code snippets:
`child: TextFormField(
controller: _emailController,
// The validator receives the text that the user has entered.
validator: (val) => !EmailValidator.validate(val!, true)
? 'Please enter a valid email.'
: null,
onSaved: (email2save) => this.email2save = email2save,
decoration: InputDecoration(
icon: Icon(
Icons.email_outlined,
color: Color(0xFF0000CC),
),
hintText: "Please enter your email",
border: InputBorder.none),
),
.......
Column(
children: [
Text('Please tick here'),
MyStatefulWidget(),
Text(' if over 18'),
],
),
.......
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool isChecked = false;
#override
Widget build(BuildContext context) {
Color getColor(Set<MaterialState> states) {
const Set<MaterialState> interactiveStates = <MaterialState>{
MaterialState.pressed,
MaterialState.hovered,
MaterialState.focused,
};
if (states.any(interactiveStates.contains)) {
return Color(0xFF0000CC);
}
return Colors.black;
}
return Checkbox(
checkColor: Colors.white,
fillColor: MaterialStateProperty.resolveWith(getColor),
value: isChecked,
onChanged: (bool? value) {
setState(() {
isChecked = value!;
});
},
);
}
Hi I don't see any Firebase in the code you've provided above but it is actually pretty easy to add two or even many things at once to Cloud Firestore:
var collection = FirebaseFirestore.instance
.collection(collection_name)
.doc(document_name);
collection
.set({
'email': 'email2save',
'check': 'isChecked'
})
You can do this anywhere in your class and it will work like magic.

Text not scrolls to the end of line in Textformfield with custom keyboard

I create a TextFormField using custom keyboard defined with this library. It works as expected except for one thing: when the text I write is longer than the TextFormField, ended text is hidden. The only way to see it is to click on the TextFormField to force the focus.
I also noticed that by clicking on the up or down arrow corresponding to nextFocus function, I can write to the corresponding TextFormField, but the cursor is not visible. I have to click on the TextField to force the focus and see the cursor. But the scroll problem always remains.
I tried also following this tutorial but it's the same.
I followed this solution but that does not solve the problem.
So do you have a good solution?
final FocusNode _nodeText1 = FocusNode();
final FocusNode _nodeText2 = FocusNode();
var appNotifier = ValueNotifier<String>("");
var keyNotifier = ValueNotifier<String>("");
KeyboardActionsConfig _buildConfig(BuildContext context) {
return KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.ALL,
keyboardBarColor: Colors.grey[200],
nextFocus: true,
actions: [
KeyboardActionsItem(
focusNode: _nodeText1,
footerBuilder: (_) => HexKeyboard(
notifier: appNotifier,
hexLength: 23,
),
),
KeyboardActionsItem(
focusNode: _nodeText2,
footerBuilder: (_) => HexKeyboard(
notifier: keyNotifier,
hexLength: 47,
),
),
],
);
}
#override
void initState() {
_appController.value =
TextEditingController.fromValue(TextEditingValue(text: "test")).value;
_appkeyController.value =
TextEditingController.fromValue(TextEditingValue(text: "hello")).value;
}
appNotifier = ValueNotifier<String>(_appController.text);
keyNotifier = ValueNotifier<String>(_keyController.text);
super.initState();
}
#override
Widget build(BuildContext context) {
body: KeyboardActions(
tapOutsideBehavior: TapOutsideBehavior.translucentDismiss,
config: _buildConfig(context),
child:...
KeyboardCustomInput<String>(
focusNode: _nodeText1,
notifier: appNotifier,
builder: (context, val, hasFocus) {
_appController.text = val;
_appController.selection =
TextSelection.fromPosition(
TextPosition(
offset:
_appController.text.length),
);
return myTextFormField(
controller: _appController,
maxLength: 23,
keyboardType: TextInputType.none,
focusNode: _nodeText1,
);
}),
KeyboardCustomInput<String>(
focusNode: _nodeText2,
notifier: keyNotifier,
builder: (context, val, hasFocus) {
_keyController.text = val;
_keyController.selection =
TextSelection.fromPosition(
TextPosition(
offset:
_keyController.text.length),
);
return myTextFormField(
controller: _appkeyController,
maxLength: 47,
keyboardType: TextInputType.none,
focusNode: _nodeText2,
showCursor: true);
}),

onEditingComplete is not called after unfocus

I have a TextField like this. The additional code is necessary to show that in different situations, I do various focus manipulation.
final node = FocusScope.of(context);
Function cleanInput = () => {controller.text = controller.text.trim()};
Function onEditingComplete;
Function onSubmitted
TextInputAction textInputAction;
if (!isLast) {
onEditingComplete = () => {
cleanInput(),
node.nextFocus(),
};
onSubmitted = (_) => {cleanInput()};
textInputAction = TextInputAction.next;
} else {
onEditingComplete = () => {
cleanInput(),
};
onSubmitted = (_) => {
cleanInput(),
node.unfocus(),
};
textInputAction = TextInputAction.done;
}
Widget textInput = TextField(
textInputAction: textInputAction,
controller: controller,
onEditingComplete: onEditingComplete,
onSubmitted: onSubmitted,
keyboardType: textInputType,
));
As you can see, I have functions I want to run onEditingComplete. However, this only gets called when I press the Next or Done buttons on my keyboard (or the Enter key in an emulator). If I change focus by tapping on a different field, this function does not get called.
I have tried using a Focus or FocusNode to help with this, but when I do so, the onEditingComplete function itself no longer works.
How can I get the desired effect here while everything plays nicely together?
Focus widget
Wrapping fields in a Focus widget might do the trick.
The Focus widget will capture focus loss events for children. With its onFocusChange argument you can call arbitrary functions.
Meanwhile, the onEditingComplete argument of TextField is unaffected and will still be called on the software keyboard "Next/Done" keypress.
This should handle field focus loss for both "Next/Done" keypress and user tapping on another field.
import 'package:flutter/material.dart';
class TextFieldFocusPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// ↓ Add this wrapper
Focus(
child: TextField(
autofocus: true,
decoration: InputDecoration(
labelText: 'Name'
),
textInputAction: TextInputAction.next,
// ↓ Handle focus change on Next / Done soft keyboard keys
onEditingComplete: () {
print('Name editing complete');
FocusScope.of(context).nextFocus();
},
),
canRequestFocus: false,
// ↓ Focus widget handler e.g. user taps elsewhere
onFocusChange: (hasFocus) {
hasFocus ? print('Name GAINED focus') : print('Name LOST focus');
},
),
TextField(
decoration: InputDecoration(
labelText: 'Password'
),
),
],
),
),
),
);
}
}
Please add a focus node to your textfield and add a listener to your focus node to trigger when it unfocuses
final node = FocusScope.of(context);
node.addListener(_handleFocusChange);
void _handleFocusChange() {
if (node.hasFocus != _focused) {
setState(() {
_focused = node.hasFocus;
});
}
}
Widget textInput = TextField(
//you missed this line of code
focusNode: node,
textInputAction: textInputAction,
controller: controller,
onEditingComplete: onEditingComplete,
onSubmitted: onSubmitted,
keyboardType: textInputType,
));
And also you can validete automatically by adding autoValidate to your code like below:
Widget textInput = TextField(
//add this line of code to auto validate
autoValidate: true,
textInputAction: textInputAction,
controller: controller,
onEditingComplete: onEditingComplete,
onSubmitted: onSubmitted,
keyboardType: textInputType,
));
FocusNode _node;
bool _focused = false;
#override
void initState() {
super.initState();
_node.addListener(_handleFocusChange);
}
void _handleFocusChange() {
if (_node.hasFocus != _focused) {
setState(() {
_focused = _node.hasFocus;
});
}
}
#override
void dispose() {
_node.removeListener(_handleFocusChange);
_node.dispose();
super.dispose();
}
TextFormField(
focusNode: _node)

TextFormField loses value

I have a PageView having 4 children/pages/steps.
On the first page I have a TextFormField. The user is obligated to provide a value.
When following exactly these steps, wrongly it will result in an error message for the user 'Please provide a name':
page is created
type a value into the TextFormField
Tap 'Done' on the soft keyboard
Tap the Next-button to go to page 2 of the PageView
Tap the Back-button to go to page 1 of the PageView
Tap the Next-button to go to page 2 of the PageView: now 'Please provide a name' is shown to the user. The typed name is still visible on the page, but its value in its validator is empty.
Deviation from the above will not cause the error message. Relevant code:
class CreateWishlist extends StatefulWidget {
#override
_CreateWishlistState createState() => _CreateWishlistState();
}
class _CreateWishlistState extends State<CreateWishlist> {
...
final myTitleController = TextEditingController();
final _form1 = GlobalKey<FormState>();
Form(
key: _form1,
child: Column(
children: <Widget>[
TextFormField(
decoration: InputDecoration(hintText: 'Name'),
textInputAction: TextInputAction.done,
onSaved: (value) {
_createWishlistData['title'] = value.trim();
},
controller: myTitleController,
validator: (value) {
if (value.trim().isEmpty) return 'Please provide a name';
return null;
},
),
],
),
),
next(int pageIndex) {
if (pageIndex == 0) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
if (!_form1.currentState.validate()) {
return;
}
_form1.currentState.save();
}
controller.animateToPage(pageIndex + 1,
duration: Duration(seconds: 1), curve: Curves.ease);
}
I tried as described in the comments above, but still the same. Eventually I managed to avoid the problem by overwriting the value on the validator and on onSaved by the controller-value.
TextFormField(
style: formFieldTextStyle,
decoration: InputDecoration(hintText: 'Name'),
textInputAction: TextInputAction.done,
onSaved: (value) {
value = myTitleController.text;
_createWishlistData['title'] = value.trim();
},
controller: myTitleController,
validator: (value) {
value = myTitleController.text;
if (value.trim().isEmpty) return 'Please provide a name';
return null;
},
),

The setter 'setValue=' was called on null

I am trying to assign an input value into my model in order to use it in the API POST request. However, I keep getting the error:
setter was called on null when saving
Model:
#JsonSerializable()
class Discount {
String value;
Discount();
set setValue(String value) {
this.value = value;
}
Dart Code:
children: <Widget>[
Expanded(
child: Container(
margin: EdgeInsets.only(right: 5),
child: TextFormField(
initialValue: _invoiceFormData.discount?.value ?? '0.00',
inputFormatters: [_amountValidator],
keyboardType: TextInputType.numberWithOptions(
decimal: true,
signed: false,
),
decoration: TextFormField.decoration(
labelText: Localizations.of(context)
.text('label_invoice_discount'),
),
onChanged: (String value) {
setState(() {
discountTotal = value;
});
},
onSaved: (value) => _invoiceFormData
.discount.setValue = value,
),
),
),
FlatButton(
onPressed: () {
setState(() {
discountType = !discountType;
});
},
),
],
Log:
The setter 'setValue=' was called on null.
Receiver: null
Tried calling: setValue="10.00"
I tried to set _invoiceFormData.discount.value = value but it is still showing the same error.
use StatefulWidget class and
change this
onSaved: (value) => _invoiceFormData
.discount.setValue = value,
to
onSaved: (value) => setState(() {
onSaved: (value) => _invoiceFormData
.discount.setValue = value,
});
Your log is saying that _invoiceFormData.discount is null when you are using the setter setValue=.
To resolve this, you first need to instantiate the field discount in your object named _invoiceFormData. For example, when you initialize this form data (maybe in your initState()), you should do _invoiceFormData.discount = Discount(); then it will not be null anymore