I created a slider-based stepper form using TabBarView
which validate the input before switching. It works, but when I go back, the state was reset. This behavior leads me to an empty form when I try to collect the data at the end of the tab.
I have googled for few hours and have been tried switching the current GetView<MyController>
to the classic StatefulWidget with AutomaticKeepAliveMixin
with no luck, so I revert it.
I'm a bit stuck, I wonder if there is any other way to achieve this, the GetX way, if possible.
visual explanation
create_account_form_slider.dart
class CreateAccountFormSlider extends GetView<CreateAccountController> {
@override
Widget build(BuildContext context) {
return Expanded(
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: controller.tabController,
children: [
_buildEmailForm(),
_buildNameForm(),
_buildPasswordForm(),
],
),
);
}
Widget _buildEmailForm() {
return Form(
key: controller.emailFormKey,
child: Column(
children: [
Spacer(), // Necessary to push the input to the bottom constraint, Align class doesn't work.
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: FormInput(
focusNode: controller.emailFocusNode,
margin: EdgeInsets.zero,
label: 'create_account_form_email'.tr,
hintText: 'janedoe@example.com',
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
validator: controller.emailValidator,
onFieldSubmitted: (_) => controller.next(),
),
),
],
),
);
}
... each form has similar structure (almost identical), so i will not include it here
create_account_controller.dart
class CreateAccountController extends GetxController
with SingleGetTickerProviderMixin {
final tabIndex = 0.obs;
final emailFormKey = GlobalKey<FormState>();
FormState get emailForm => emailFormKey.currentState;
final emailFocusNode = FocusNode();
final email = ''.obs;
TabController tabController;
@override
void onInit() {
_initTabController();
super.onInit();
}
@override
void onClose() {
_disposeFocusNodes();
_disposeTabController();
super.onClose();
}
/// Initialize tab controller and add a listener.
void _initTabController() {
tabController = TabController(vsync: this, length: 3);
tabController.addListener(_tabListener);
}
/// Listen on tab change and update `tabIndex`
void _tabListener() => tabIndex(tabController.index);
/// Dispose tab controller and remove its listener.
void _disposeTabController() {
tabController.removeListener(_tabListener);
tabController.dispose();
}
/// Dispose all the focus nodes.
void _disposeFocusNodes() {
emailFocusNode.dispose();
}
/// Animate to the next slide.
void _nextSlide() => tabController.animateTo(tabIndex() + 1);
/// Animate to the next slide or submit if current tab is the last tab.
void next() {
if (tabIndex().isEqual(0) && emailForm.validate()) {
_nextSlide();
return focusScope.requestFocus(nameFocusNode);
}
...
}
/// A function that checks the validity of the given value.
///
/// When the email is empty, show required error message and when the email
/// is invalid, show the invalid message.
String emailValidator(String val) {
if (val.isEmpty) return 'create_account_form_error_email_required'.tr;
if (!val.isEmail) return 'create_account_form_error_email_invalid'.tr;
return null;
}
/// Submit data to the server.
void _submit() {
print('TODO: implement submit');
print(email());
}
}
I made it by saving the form and adding an initialValue
on my custom FormInput
widget then put the observable variable onto each related FormInput
. No need to use keepalive mixin.
create_account_controller.dart
/// Animate to the next slide or submit if current tab is the last tab.
void next() {
if (tabIndex().isEqual(0) && emailForm.validate()) {
// save the form so the value persisted into the .obs variable
emailForm.save();
// slide to next form
_nextSlide();
// TODO: wouldn't it be nice if we use autofocus since we only have one input each form?
return focusScope.requestFocus(nameFocusNode);
}
...
}
create_account_form_slider.dart
Obx( // wrap the input inside an Obx to rebuild with the new value
() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: FormInput(
focusNode: controller.emailFocusNode,
label: 'create_account_form_email'.tr,
hintText: 'janedoe@example.com',
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
validator: controller.emailValidator,
onFieldSubmitted: (_) => controller.next(),
initialValue: controller.email(), // use initial value to keep current value when user go back from the next slide
onSaved: controller.email, // persist current value into the .obs variable
),
),
),
FYI: The FormInput
is just a regular TextInput
, only decoration
is modified. This should work with the regular flutter TextInput
.