Flutter电话号码格式化工具报错:Range start超出文本长度
Flutter电话号码格式化工具越界错误修复
问题背景
开发Flutter电话号码格式化工具,需求如下:
- 国家码以
1开头时,格式化后总长度为13(示例:'+1 222333444') - 国家码以其他数字开头时,格式化后总长度为14(示例:
'+99 222333444')
自定义NumberTextInputFormatter实现后,运行时持续抛出Range start X is out of text of length Y越界错误,即使已做截断处理,输入时仍不断触发该错误。
原代码与报错信息
格式化工具代码
import 'package:flutter/services.dart'; class NumberTextInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue) { final int newTextLength = newValue.text.length; int selectionIndex = newValue.selection.end; int usedSubstringIndex = 0; final StringBuffer newText = StringBuffer(); if (newTextLength >= 1) { newText.write('+'); if (newValue.selection.end >= 1) selectionIndex++; } if (newTextLength >= 3) { if (newValue.text.substring(0, usedSubstringIndex = 1) == '1') { newText.write('${newValue.text.substring(0, usedSubstringIndex = 1)} '); } else { newText.write('${newValue.text.substring(0, usedSubstringIndex = 2)} '); } if (newValue.selection.end >= 2) selectionIndex += 1; } // Dump the rest. if (newTextLength >= usedSubstringIndex) { newText.write(newValue.text.substring(usedSubstringIndex)); } return TextEditingValue( text: truncateBuffer(newText, usedSubstringIndex+12), selection: TextSelection.collapsed(offset: selectionIndex), ); } static String truncateBuffer(StringBuffer value, int maxLength) { print(value.length); print(maxLength); if (value.length>maxLength) { print('truncating triggered~~~'); return value.toString().substring(0, maxLength) ; }else { return value.toString(); } } }
报错堆栈信息
══╡ EXCEPTION CAUGHT BY SERVICES LIBRARY ╞═════════════════════════════════════════════════════════ The following assertion was thrown during method call TextInputClient.updateEditingState: Range start 14 is out of text of length 13 'package:flutter/src/services/text_input.dart': Failed assertion: line 1015 pos 12: 'range.start >= 0 && range.start <= text.length' Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause. In either case, please report this assertion by filing a bug on GitHub: https://github.com/flutter/flutter/issues/new?templa te=2_bug.yml When the exception was thrown, this was the stack: #2 TextEditingValue._textRangeIsValid (package:flutter/src/services/text_input.dart:1015:12) #3 TextEditingValue.toJSON (package:flutter/src/services/text_input.dart:970:12) #4 _PlatformTextInputControl.setEditingState (package:flutter/src/services/text_input.dart:2312:13) #5 TextInput._setEditingState (package:flutter/src/services/text_input.dart:1993:15) #6 TextInputConnection.setEditingState (package:flutter/src/services/text_input.dart:1435:25) #7 EditableTextState._updateRemoteEditingValueIfNeeded (package:flutter/src/widgets/editable_text.dart:3525:27) #8 EditableTextState.endBatchEdit (package:flutter/src/widgets/editable_text.dart:3514:5) #9 EditableTextState._formatAndSetValue (package:flutter/src/widgets/editable_text.dart:4208:5) #10 EditableTextState.updateEditingValue (package:flutter/src/widgets/editable_text.dart:3246:7) #11 TextInput._updateEditingValue (package:flutter/src/services/text_input.dart:2067:43) #12 TextInput._handleTextInputInvocation (package:flutter/src/services/text_input.dart:1903:29) #13 TextInput._loudlyHandleTextInputInvocation (package:flutter/src/services/text_input.dart:1803:20) #14 MethodChannel._handleAsMethodCall (package:flutter/src/services/platform_channel.dart:571:55) #15 MethodChannel.setMethodCallHandler.<anonymous closure> (package:flutter/src/services/platform_channel.dart:564:34) #16 _DefaultBinaryMessenger.setMessageHandler.<anonymous closure> (package:flutter/src/services/binding.dart:618:35) #17 _invoke2 (dart:ui/hooks.dart:344:13) #18 _ChannelCallbackRecord.invoke (dart:ui/channel_buffers.dart:45:5) #19 _Channel.push (dart:ui/channel_buffers.dart:135:31) #20 ChannelBuffers.push (dart:ui/channel_buffers.dart:343:17) #21 PlatformDispatcher._dispatchPlatformMessage (dart:ui/platform_dispatcher.dart:750:22) #22 _dispatchPlatformMessage (dart:ui/hooks.dart:257:31) (elided 2 frames from class _AssertionError) call: MethodCall(TextInputClient.updateEditingState, [1, {selectionBase: 14, composingExtent: -1, selectionAffinity: TextAffinity.downstream, text: +1 22233344445, selectionIsDirectional: false, selectionExtent: 14, composingBase: -1}]) ══════════════════════════════════════════════════════════════════════════════════
输入时重复报错
... Another exception was thrown: Range start 37 is out of text of length 13 flutter: 38 flutter: 13 flutter: truncating triggered~~~ Another exception was thrown: Range start 38 is out of text of length 13 flutter: 39 flutter: 13 flutter: truncating triggered~~~ Another exception was thrown: Range start 39 is out of text of length 13 flutter: 40 flutter: 13 flutter: truncating triggered~~~ Another exception was thrown: Range start 40 is out of text of length 13 ...
页面实现代码
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:heard/ui/views/phone_formatter.dart'; class PeopleForm extends StatefulWidget { @override State createState() => _PeopleFormState(); } class _PeopleFormState extends State<PeopleForm> { final _formKey = GlobalKey<FormBuilderState>(); final _mobileFormatter = NumberTextInputFormatter(); @override Widget build(BuildContext context) { return FormBuilder( key: _formKey, child: Column( children: [ ... Row( children: [ ... Flexible( fit: FlexFit.tight, child: FormBuilderTextField( name: 'phone', keyboardType: TextInputType.number, inputFormatters: <TextInputFormatter>[ FilteringTextInputFormatter.digitsOnly, _mobileFormatter ], decoration: const InputDecoration( labelText: 'Phone Number', ), validator: FormBuilderValidators.compose([ FormBuilderValidators.required(), ]), )) ], ), ... ], ), ); } }
问题原因
- 光标位置未同步截断后的文本长度:截断文本后,光标位置仍基于原始输入长度计算,导致超出新文本范围
- 索引赋值逻辑混乱:在判断条件中直接赋值
usedSubstringIndex,导致后续逻辑依赖错误的索引值 - 长度计算错误:
usedSubstringIndex+12无法正确匹配13或14的总长度要求 - 未提前限制原始输入:处理前未截断过长的原始数字,导致后续生成超出限制的格式化文本
修复方案
重新梳理逻辑,先截断原始输入,再处理格式化,最后同步光标位置:
import 'package:flutter/services.dart'; class NumberTextInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue) { // 先提取纯数字并截断到最大允许长度 String rawDigits = newValue.text.replaceAll(RegExp(r'\D'), ''); int maxRawLength = rawDigits.startsWith('1') ? 11 : 12; if (rawDigits.length > maxRawLength) { rawDigits = rawDigits.substring(0, maxRawLength); } final StringBuffer formattedText = StringBuffer(); int selectionOffset = 0; // 添加+号 if (rawDigits.isNotEmpty) { formattedText.write('+'); selectionOffset++; } // 处理国家码和空格 if (rawDigits.length >= 1) { int countryCodeLength = rawDigits.startsWith('1') ? 1 : 2; formattedText.write(rawDigits.substring(0, countryCodeLength)); formattedText.write(' '); selectionOffset += countryCodeLength + 1; // 添加剩余数字 if (rawDigits.length > countryCodeLength) { formattedText.write(rawDigits.substring(countryCodeLength)); } } // 转换原始光标位置到格式化后的文本 int originalCursorPos = newValue.selection.end.clamp(0, rawDigits.length); if (originalCursorPos == 0) { selectionOffset = 0; } else if (originalCursorPos <= (rawDigits.startsWith('1') ? 1 : 2)) { selectionOffset = originalCursorPos + 1; } else { int baseOffset = rawDigits.startsWith('1') ? 3 : 4; selectionOffset = baseOffset + (originalCursorPos - (rawDigits.startsWith('1') ? 1 : 2)); } // 确保光标不超出格式化文本长度 selectionOffset = selectionOffset.clamp(0, formattedText.length); return TextEditingValue( text: formattedText.toString(), selection: TextSelection.collapsed(offset: selectionOffset), ); } }
关键修改说明
- 提前截断原始输入:根据国家码开头限制原始数字的最大长度,从源头上避免生成过长文本
- 简化国家码判断逻辑:直接通过原始数字开头确定国家码长度,逻辑更清晰
- 精准计算光标位置:基于原始光标在截断后数字中的位置,转换为格式化后的文本位置,并做范围校验
- 移除冗余截断逻辑:通过提前限制原始输入长度,无需再处理格式化后的文本截断,简化代码
验证效果
- 输入1开头的国家码:自动格式化为
+1 XXXXXX,总长度不超过13 - 输入其他国家码:自动格式化为
+XX XXXXXX,总长度不超过14 - 光标位置始终跟随输入,不会出现越界错误
内容的提问来源于stack exchange,提问作者Meryl
相关产品推荐
相关产品推荐

