ux_kit_core lets you manage UI behavior configuration in one place, separate from styling and decoration. For now, it focuses on inputs and forms, using predefined and flexible blocks of configs and controllers to help you handle input lifecycles, validation, and sync and async updates.

Getting started

Prerequisites:

  • Flutter 3.0 or higher
  • Dart 3.8 or higher

Usage

class MyForm extends StatefulWidget {
  const MyForm({super.key});

  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {

  late EmailInput email;
  late PasswordInput password;

  @override
  void initState() {
    super.initState();
    email = EmailInput.init(EmailConfig());
    email.addDefaultListeners();
    password = PasswordInput.init(PasswordConfig());
    password.addDefaultListeners();
  }

  @override
  void dispose() {
    email.dispose();
    password.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        MyTextField(input: email),
        MyTextField(input: password),
        ElevatedButton(
          onPressed: () {
            if (email.validate() && password.validate()) {
              print('Email: ${email.formatted}');
              print('Password: ${password.formatted}');
            }
          },
          child: const Text('Submit'),
        ),
      ],
    );
  }
}

class MyTextField extends StatefulWidget {
  const MyTextField({super.key, required this.input});

  final FieldInput input;

  @override
  State<MyTextField> createState() => _MyTextFieldState();
}

class _MyTextFieldState extends State<MyTextField> {
  bool obscureText = false;

  String? errorText;

  @override
  void initState() {
    super.initState();
    widget.input.validationResult.addListener(errorTextListener);
    if (widget.input case PasswordInput password) {
      password.obscure.addListener(obscurePasswordListener);
    }
  }

  @override
  void dispose() {
    widget.input.validationResult.removeListener(errorTextListener);
    if (widget.input case PasswordInput password) {
      password.obscure.removeListener(obscurePasswordListener);
    }
    super.dispose();
  }

  void obscurePasswordListener() {
    setState(() {
      obscureText = (widget.input as PasswordInput).obscure.value;
    });
  }

  void errorTextListener() {
    setState(() {
      final validationResult = widget.input.validationResult.value;
      if (validationResult case HasMessage(isValid: final isValid, message: final message) when !isValid && message is String) {
        errorText = message;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: widget.input.textEditing,
      focusNode: widget.input.focusNode,
      autocorrect: widget.input is! PasswordInput,
      obscureText: obscureText,
      keyboardType: widget.input.keyboardType,
      inputFormatters: widget.input.inputFormatters,
      autofillHints: widget.input.autofillHints,
      decoration: InputDecoration(
        labelText: widget.input.config.label,
        hintText: widget.input.config.hint,
        errorText: errorText,
      ),
    );
  }
}

extension on FieldInput {
  TextInputType get keyboardType {
    return switch (this) {
      PasswordInput() => TextInputType.visiblePassword,
      PhoneInput() => TextInputType.phone,
      TextInput() => TextInputType.text,
      NumberInput() => TextInputType.number,
      EmailInput() => TextInputType.emailAddress,
      DateInput() => TextInputType.datetime,
      AddressInput() => TextInputType.streetAddress,
    };
  }

  List<TextInputFormatter>? get inputFormatters {
    return switch (this) {
      PhoneInput() ||
      NumberInput() => [FilteringTextInputFormatter.digitsOnly],
      _ => null,
    };
  }

  List<String>? get autofillHints {
    return switch (this) {
      PasswordInput() => [AutofillHints.password],
      PhoneInput() => [
        AutofillHints.telephoneNumberNational,
        AutofillHints.telephoneNumber,
      ],
      EmailInput() => [AutofillHints.email],
      DateInput() => [AutofillHints.birthday],
      AddressInput() => [AutofillHints.fullStreetAddress],
      _ => null,
    };
  }
}