fluttertextfieldtexteditingcontrollertextspan

Selecting text in TextField returns error "range.end >= 0 && range.end <= text.length" Flutter


I have an amount input text field with custom TextEditingController that has symbol e.i. 200 $, when I try to select the whole text |2 000 $| (| - selection pins), an exception is thrown. I have tried to do it in the level of input formatter, but my symbol has different text style


    ════════ Exception caught by gesture ═══════════════════════════════════════════
    Range end 12 is out of text of length 7
    'package:flutter/src/services/text_input.dart':
    Failed assertion: line 968 pos 12: 'range.end >= 0 && range.end <= text.length'
    ════════════════════════════════════════════════════════════════════════════════

    [AppMetricaPlugin.MainFacade] error caught by handler Range end 10 is out of text of length 7
[AppMetricaPlugin.MainFacade] _AssertionError ('package:flutter/src/services/text_input.dart': Failed assertion: line 968 pos 12: 'range.end >= 0 && range.end <= text.length': Range end 10 is out of text of length 7)
[AppMetricaPlugin.MainFacade] #0      _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:51:61)
                              #1      _AssertionError._throwNew (dart:core-patch/errors_patch.dart:40:5)
                              #2      TextEditingValue._textRangeIsValid (package:flutter/src/services/text_input.dart:968:12)
                              #3      TextEditingValue.toJSON (package:flutter/src/services/text_input.dart:921:12)
                              #4      _PlatformTextInputControl.setEditingState (package:flutter/src/services/text_input.dart:2276:13)
                              #5      TextInput._setEditingState (package:flutter/src/services/text_input.dart:1957:15)
                              #6      TextInputConnection.setEditingState (package:flutter/src/services/text_input.dart:1381:25)
                              #7      EditableTextState._updateRemoteEditingValueIfNeeded (package:flutter/src/widgets/editable_text.dart:3379:27)
                              #8      EditableTextState.endBatchEdit (package:flutter/src/widgets/editable_text.dart:3368:5)
                              #9      EditableTextState._formatAndSetValue (package:flutter/src/widgets/editable_text.dart:3919:5)
                              #10     EditableTextState.userUpdateTextEditingValue (package:flutter/src/widgets/editable_text.dart:4271:5)
                              #11     TextSelectionOverlay._handleSelectionHandleChanged (package:flutter/src/widgets/text_selection.dart:900:23)
                              #12     TextSelectionOverlay._handleSelectionEndHandleDragUpdate (package:flutter/src/widgets/text_selection.dart:776:5)
                              #13     SelectionOverlay._handleEndHandleDragUpdate (package:flutter/src/widgets/text_selection.dart:1212:28)
                              #14     DragGestureRecognizer._checkUpdate.<anonymous closure> (package:flutter/src/gestures/monodrag.dart:581:55)
                              #15     GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:315:24)
                              #16     DragGestureRecognizer._checkUpdate (package:flutter/src/gestures/monodrag.dart:581:7)
                              #17     DragGestureRecognizer.handleEvent (package:flutter/src/gestures/monodrag.dart:422:9)
                              #18     PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:98:12)
                              #19     PointerRouter._dispatchEventToRoutes.<anonymous closure> (package:flutter/src/gestures/pointer_router.dart:143:9)
                              #20     _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:633:13)
                              #21     PointerRouter._dispatchEventToRoutes (package:flutter/src/gestures/pointer_router.dart:141:18)
                              #22     PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:127:7)
                              #23     GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:495:19)
                              #24     GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:475:22)
                              #25     RendererBinding.dispatchEvent (package:flutter/src/rendering/binding.dart:430:11)
                              #26     GestureBinding._handlePointerEventImmediately (package:flutter/src/gestures/binding.dart:420:7)
                              #27     GestureBinding.handlePointerEvent (package:flutter/src/gestures/binding.dart:383:5)
                              #28     GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:330:7)
                              #29     GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:299:9)
                              #30     _rootRunUnary (dart:async/zone.dart:1415:13)
                              #31     _CustomZone.runUnary (dart:async/zone.dart:1308:19)
                              #32     _CustomZone.runUnaryGuarded (dart:async/zone.dart:1217:7)
                              #33     _invoke1 (dart:ui/hooks.dart:330:10)
                              #34     PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:429:7)
                              #35     _dispatchPointerDataPacket (dart:ui/hooks.dart:262:31)

---------------- My custom TextEditingController

class AmountBigController extends TextEditingController {
  AmountBigController({
    required this.symbol,
    required this.focusNode,
    this.includeSymbolInHint = true,
    String? hintText,
    super.text = '',
  }) : _hintText = hintText;

  final String symbol;
  final FocusNode focusNode;
  final bool includeSymbolInHint;
  String? _hintText;

  void setHintText(String text) {
    _hintText = text;
    notifyListeners();
  }

  @override
  void clear() {
    value = const TextEditingValue(
      text: '',
      selection: TextSelection.collapsed(offset: 0),
    );
  }

  @override
  TextSpan buildTextSpan({
    required BuildContext context,
    required bool withComposing,
    TextStyle? style,
  }) {
    final List<TextSpan> children = [];
    final isHintShown = focusNode.hasFocus && value.text.isEmpty;

    final TextStyle? textStyle =
        isHintShown ? Typographies.textMediumHelper : style;

    late final TextSpan symbolTextSpan;
    if (isHintShown) {
      if (includeSymbolInHint) {
        symbolTextSpan = TextSpan(
          text: symbol,
          style: textStyle,
        );
      } else {
        symbolTextSpan = TextSpan(
          text: '',
          style: textStyle,
        );
      }
    } else {
      symbolTextSpan = TextSpan(
        text: symbol,
        style: textStyle,
      );
    }

    if (text.isEmpty) {
      if (focusNode.hasFocus) {
        children.insert(
          0,
          TextSpan(text: _hintText, style: Typographies.textMediumHelper),
        );
      } else {
        children.insert(
          0,
          TextSpan(
            text: '0',
            style: style,
          ),
        );
      }
    } else {
      children.add(TextSpan(style: style, text: text));
    }

    children.insert(children.length, symbolTextSpan);
    return TextSpan(children: children);
  }
}

Solution

  • You are getting the error because the text value in AmountBigController is not in sync with the result of buildTextSpan.

    TextEditingValue

    Your AmountBigController has a TextEditingValue property, which describes the current text value and selection value of the TextField. (The selection value also includes the current cursor position). For example, when you call the text getter in this line:

    children.add(TextSpan(style: style, text: text)
    

    you are getting the current text saved in TextEditingValue

    buildTextSpan

    The actual displaying of the text is managed by the method buildTextSpan. You have overridden this method and returned a new TextSpan. In your TextSpan, the length of the text is increased by one -- you add a $ symbol to the end.

    The problem

    So the text displays as '2000$' , but the internal text value has not been updated, so is actually '2000'. When you select the text, the AmountBigController tries to update the selection property to something like: TextRange(start: 0, end: 5). Now the TextEditingValue has a text value of '2000' and a selection value that stretches over 5 characters.

    So now you get a range error because you cannot have a TextSelection that is longer than the length of the text.

    The solution

    It is fine to set different styles in the TextField by updating buildTextSpan. However, you need to make sure the TextEditingValue is updated to reflect this. So first, create a TextInputFormatter like this one:

    class SymbolHintFormatter extends TextInputFormatter {
      final String symbol;
    
      SymbolHintFormatter({required this.symbol});
      @override
      TextEditingValue formatEditUpdate(
          TextEditingValue oldValue, TextEditingValue newValue) {
        String newText = newValue.text;
        if (newValue.text.isEmpty) {
          newText = symbol;
        } else {
          newText = newText.replaceAll(symbol, '');
          newText = '$symbol$newText';
        }
        return newValue.copyWith(text: newText);
      }
    }
    

    Using this SymbolHintFormatter means the symbol will be in the TextEditingControllers text property.

    Add this to your TextField:

       TextField(
                inputFormatters:
                    symbol != null ? [SymbolHintFormatter(symbol: symbol!)] : [],
                controller: AmountBigController(
                  symbol: symbol ?? '',
                  focusNode: focusNode,
                  includeSymbolInHint: symbol != null,
                ),
              ),
    

    Now you can change buildTextSpan. Adding the symbol as a separate TextSpan child will now be fine, since the TextEditingValue also has the symbol in its text property:

    @override
      TextSpan buildTextSpan({
        required BuildContext context,
        TextStyle? style,
        required bool withComposing,
      }) {
        final List<TextSpan> children = [];
        final isHintShown = focusNode.hasFocus && value.text == symbol;
    
        if (includeSymbolInHint && isHintShown) {
          final textStyle = isHintShown ? Typographies.textMediumHelper : style;
          children.add(TextSpan(text: symbol, style: textStyle));
          children.add(TextSpan(text: text..replaceAll(symbol, '')));
          return TextSpan(children: children);
        } else {
          return TextSpan(text: text, style: style);
        }
      }