Search code examples
flutterflutter-getx

Is there any workaround for using keepalive mixin alongside the GetX package?


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

enter image description here`

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());
  }
}

Solution

  • 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.