1

How can I update the PinInput Boxes with input from the on-screen keyboard? From what I understand, whenever there's a state change, the widget will be rebuild. Hence, from below, what I did was updating the text whenever the on-screen keyboard detects tap. Then since the state is changed, I assumed it will rebuild all the widget which include the PinInput widget, and this is true since I tested the text whenever there's changes. I then did _pinPutController.text = text; to change the input of PinInput, however it is not working.

When I hardcode _pinPutController.text = '123', it works. So the problem is that it is not rebuilding. Am I understanding this correctly? How can I achieve what I wanted?

import 'package:flutter/material.dart';
import 'package:numeric_keyboard/numeric_keyboard.dart';
import 'package:pinput/pin_put/pin_put.dart';

import '../../../../constants.dart';
import '../../../../size_config.dart';

class InputForm extends StatefulWidget {
  @override
  _InputFormState createState() => _InputFormState();
}

class _InputFormState extends State<InputForm> {
  String text = '';

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        PinInput(text: text),
        NumericKeyboard(
            onKeyboardTap: (value) {
              setState(() {
                text += value;
              });
            },
            textColor: Colors.red,
            rightButtonFn: () {
              setState(() {
                text = text.substring(0, text.length - 1);
              });
            },
            rightIcon: Icon(
              Icons.backspace,
              color: Colors.red,
            ),
            mainAxisAlignment: MainAxisAlignment.spaceEvenly),
      ],
    );
  }
}

class PinInput extends StatelessWidget {
  const PinInput({
    Key key,
    this.text,
  }) : super(key: key);

  final String text;

  @override
  Widget build(BuildContext context) {
    final size = getProportionateScreenHeight(60);
    final TextEditingController _pinPutController = TextEditingController();
    final FocusNode _pinPutFocusNode = FocusNode();
    _pinPutFocusNode.unfocus();
    print(text);
    _pinPutController.text = text;

    return PinPut(
      fieldsCount: 4,
      onSubmit: (String pin) => {},
      focusNode: _pinPutFocusNode,
      controller: _pinPutController,
      preFilledWidget: Align(
        alignment: Alignment.bottomCenter,
        child: Divider(
          color: kPrimaryColor,
          thickness: 2.5,
          indent: 7.5,
          endIndent: 7.5,
        ),
      ),
      textStyle: TextStyle(
        fontSize: getProportionateScreenHeight(24),
      ),
      eachFieldPadding: EdgeInsets.all(
        getProportionateScreenHeight(10),
      ),
      eachFieldMargin: EdgeInsets.all(
        getProportionateScreenWidth(5),
      ),
      eachFieldHeight: size,
      eachFieldWidth: size,
      submittedFieldDecoration: boxDecoration(),
      selectedFieldDecoration: boxDecoration(),
      followingFieldDecoration: boxDecoration(),
      inputDecoration: InputDecoration(
        border: InputBorder.none,
        focusedBorder: InputBorder.none,
        enabledBorder: InputBorder.none,
        counterText: '',
      ),
      withCursor: true,
      pinAnimationType: PinAnimationType.scale,
      animationDuration: kAnimationDuration,
    );
  }

  BoxDecoration boxDecoration() {
    return BoxDecoration(
      color: Colors.white,
      shape: BoxShape.rectangle,
      borderRadius: BorderRadius.circular(
        getProportionateScreenWidth(10),
      ),
    );
  }
}
Wei Chun
  • 41
  • 1
  • 5

1 Answers1

0

The problem is that you recreate a new TextEditingController at each rebuild of your PinInput Widget. However, if you check the PinPutState of the pinput package, it keeps a reference to the first TextEditingController you provide in its initState method:

@override
void initState() {
  _controller = widget.controller ?? TextEditingController();
  [...]
}

So, you have to keep the same TextEditingController all the way.

The easiest way to fix this would be to raise the TextEditingController to the State of InputForm. Instead of a String text, just use a Controller:

class InputForm extends StatefulWidget {
  @override
  _InputFormState createState() => _InputFormState();
}

class _InputFormState extends State<InputForm> {
  TextEditingController _controller = TextEditingController(text: '');

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        PinInput(controller: _controller),
        NumericKeyboard(
          onKeyboardTap: (value) => _controller.text += value,
          textColor: Colors.red,
          rightButtonFn: () => _controller.text =
              _controller.text.substring(0, _controller.text.length - 1),
          rightIcon: Icon(Icons.backspace, color: Colors.red),
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        ),
      ],
    );
  }
}

Note: Since you use a TextEditingController instead of a String, you can get rid of all your setState methods.

enter image description here

Full source code:

import 'package:flutter/material.dart';
import 'package:numeric_keyboard/numeric_keyboard.dart';
import 'package:pinput/pin_put/pin_put.dart';

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      home: HomePage(),
    ),
  );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: InputForm()),
    );
  }
}

class InputForm extends StatefulWidget {
  @override
  _InputFormState createState() => _InputFormState();
}

class _InputFormState extends State<InputForm> {
  TextEditingController _controller = TextEditingController(text: '');

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        PinInput(controller: _controller),
        NumericKeyboard(
          onKeyboardTap: (value) => _controller.text += value,
          textColor: Colors.red,
          rightButtonFn: () => _controller.text =
              _controller.text.substring(0, _controller.text.length - 1),
          rightIcon: Icon(Icons.backspace, color: Colors.red),
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        ),
      ],
    );
  }
}

class PinInput extends StatelessWidget {
  const PinInput({
    Key key,
    this.controller,
  }) : super(key: key);

  final TextEditingController controller;

  @override
  Widget build(BuildContext context) {
    final size = 60.0;
    final FocusNode _pinPutFocusNode = FocusNode();
    _pinPutFocusNode.unfocus();

    return PinPut(
      fieldsCount: 4,
      onSubmit: (String pin) => {},
      focusNode: _pinPutFocusNode,
      controller: controller,
      preFilledWidget: Align(
        alignment: Alignment.bottomCenter,
        child: Divider(
          color: Theme.of(context).primaryColor,
          thickness: 2.5,
          indent: 7.5,
          endIndent: 7.5,
        ),
      ),
      textStyle: TextStyle(
        fontSize: 24,
      ),
      eachFieldPadding: EdgeInsets.all(
        10,
      ),
      eachFieldMargin: EdgeInsets.all(
        5,
      ),
      eachFieldHeight: size,
      eachFieldWidth: size,
      submittedFieldDecoration: boxDecoration(),
      selectedFieldDecoration: boxDecoration(),
      followingFieldDecoration: boxDecoration(),
      inputDecoration: InputDecoration(
        border: InputBorder.none,
        focusedBorder: InputBorder.none,
        enabledBorder: InputBorder.none,
        counterText: '',
      ),
      withCursor: true,
      pinAnimationType: PinAnimationType.scale,
      animationDuration: Duration(milliseconds: 500),
    );
  }

  BoxDecoration boxDecoration() {
    return BoxDecoration(
      color: Colors.white,
      shape: BoxShape.rectangle,
      borderRadius: BorderRadius.circular(
        10,
      ),
    );
  }
}

UPDATE: How to hide the Keyboard...

...while keeping the focus blinking cursor

1. disable the focus on your PinPut fields:

For this, I used a class described here:

class AlwaysDisabledFocusNode extends FocusNode {
  @override
  bool get hasFocus => false;
}

...as the focusNode of the PinPut:

class PinInput extends StatelessWidget {
  [...]
  @override
  Widget build(BuildContext context) {
    final size = 60.0;
    final FocusNode _pinPutFocusNode = AlwaysDisabledFocusNode();
    // _pinPutFocusNode.unfocus();

    return PinPut(
      [...]
      focusNode: _pinPutFocusNode,
      [...]
    );
  }
}

So, now, the PinPut never gets the focus. The Soft Keyboard is not shown but, hey!, we lost the blinking cursor!

No worries, we'll keep it always on programmatically.

2. Keep the blinking cursor always on

For this, though, we'll have to change the code of the pinput package.

Currently, in PinPutState, the blinking cursor is shown on the next field if withCursor is set to true and the PinPut has the focus. Instead, we will always show the blinking cursor if withCursoris set to true:

if (widget.withCursor /* && _focusNode!.hasFocus */ && index == pin.length) {
  return _buildCursor();
}

Voilà! Does it work for you?

This has been mentioned on PinPut GitHub Issue Tracker [ref] about disabling the device keyboard when using a custom keyboard.

Thierry
  • 7,775
  • 2
  • 15
  • 33
  • Thank @Thierry for the clear and concise answer. Another question is that how can I disable the OS keyboard that comes up every-time there's focus? Autofocus should remain true, but just without the OS keyboard – Wei Chun Mar 10 '21 at 07:40
  • I might have a solution for that. – Thierry Mar 10 '21 at 09:24