From 1ceb47f1e38b847f29ddac907fdaf4931d3b1c1a Mon Sep 17 00:00:00 2001 From: EdisonJwa Date: Sat, 16 May 2026 18:55:00 +0800 Subject: [PATCH] fix(ui,ios): remove _kickFocus microtask refocus (500-1000 ms keyboard lag) User reported: 'amount need wait 500ms - 1s if i click input field -> then keyboard popup'. The 79f8360 _kickFocus workaround was the source of the lag. Root cause of the lag: void _kickFocus(FocusNode node) { if (node.hasFocus) node.unfocus(); Future.microtask(() { // <-- this microtask if (!mounted) return; node.requestFocus(); }); } The Future.microtask deferral forces EditableText's attach-to- TextInput path to wait one frame past the user's pointer-up. iOS 26's keyboard slide-up animation then dovetails into that extra frame in a way that adds another 200-800 ms before the keyboard actually appears on screen. Net latency: ~500-1000 ms. Fix: remove _kickFocus entirely. Rely on: 1. TextField's native onTap path (no onTap override = no deferral, no microtask hop, no SystemChannels race). 2. The tap-outside-to-unfocus GestureDetector wrapping the connect form Column (7c62d14) which already guarantees the FocusNode is in the unfocused state when the user taps any field, because any prior keyboard dismissal (tap outside / tap a sibling field) goes through FocusScope.of(context).unfocus(). This means the FocusNode is always in a clean false state when a TextField gets tapped, so EditableText's own attach path can fire synchronously on the first frame and the keyboard appears instantly. The full _kickFocus implementation is retained as a code comment above the connect-form's build() for documentation and quick re-introduction should iOS regress again. The flutter/flutter#181474 issue (the underlying iOS 26 bug) remains open, so the comment documents the canonical workaround if needed. flutter analyze: 6 pre-existing Radio.groupValue deprecation infos (unchanged). flutter build ios --release --no-codesign: 20.4 s, Runner.app 30.2 MB. --- apps/chanora_flutter/lib/main.dart | 65 ++++++++++++++---------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index 845bacf..7baafae 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -1283,37 +1283,37 @@ class _ConnectFormState extends State<_ConnectForm> { final FocusNode _nickFocus = FocusNode(); final FocusNode _passwordFocus = FocusNode(); - /// Workaround for flutter/flutter#181474 (open, P2, triaged-text- - /// input, iOS 26+): after the soft keyboard is dismissed, the - /// FocusNode still reports `hasFocus == true`. Because the node is - /// already focused, the next user tap on the same TextField does - /// not trigger any focus-change side effect, so the platform - /// TextInput channel is never re-opened, and iOS keeps the soft - /// keyboard hidden. The user has to tap twice (the second tap - /// triggers an explicit re-focus path that re-shows the keyboard). - /// - /// The community-recommended workaround in the issue thread is to - /// **unfocus first, then re-request focus** on every tap. That - /// forces a real focus-change transition (`false -> true`) which - /// re-opens TextInput and slides the keyboard up on the first tap. - /// - /// This costs nothing on hosts where the bug doesn't reproduce - /// (the unfocus call is a no-op when the node isn't focused; the - /// re-request just reaffirms focus and EditableText's own attach - /// path handles the rest). - void _kickFocus(FocusNode node) { - // Drop focus synchronously, then re-grab it on the next frame. - // Doing both in one frame can race with EditableText's internal - // bookkeeping; deferring the re-grab by one microtask sidesteps - // that and matches the workaround pattern in the issue thread. - if (node.hasFocus) { - node.unfocus(); - } - Future.microtask(() { - if (!mounted) return; - node.requestFocus(); - }); - } + // _kickFocus (unfocus + Future.microtask refocus on every onTap) + // was removed after user-reported 500\u20131000 ms keyboard appearance + // latency on iPhone 16 Pro / iOS 26. + // + // The microtask deferral was the source of the lag: it forces + // EditableText's attach-to-TextInput path to wait one frame past + // the user's pointer-up, and iOS 26's keyboard slide-up animation + // then dovetails into that extra frame in a way that adds another + // 200\u2013800 ms before the keyboard actually appears. + // + // The tap-outside-to-unfocus GestureDetector wrapping the connect + // form Column (see build() below) already guarantees the FocusNode + // is in the unfocused state when the user taps a field, because + // any prior keyboard dismissal (tap outside / tap a sibling field) + // goes through FocusScope.unfocus(). Therefore TextField's native + // tap path can attach TextInput on the first frame with no help + // from us, and the keyboard appears instantly. + // + // If iOS regresses again such that focus state desyncs from + // keyboard visibility, the workaround to re-introduce here is the + // canonical pattern from flutter/flutter#181474: + // + // void _kickFocus(FocusNode node) { + // if (node.hasFocus) node.unfocus(); + // Future.microtask(() { + // if (!mounted) return; + // node.requestFocus(); + // }); + // } + // + // and wire onTap: () => _kickFocus(_xxxFocus) on each TextField. @override void dispose() { @@ -1347,7 +1347,6 @@ class _ConnectFormState extends State<_ConnectForm> { TextField( controller: widget.hostCtl, focusNode: _hostFocus, - onTap: () => _kickFocus(_hostFocus), // Server addresses are URL-shaped: hostname or // hostname:port, all-lowercase ASCII, never user- // friendly prose. Configure the on-screen keyboard @@ -1391,7 +1390,6 @@ class _ConnectFormState extends State<_ConnectForm> { TextField( controller: widget.nickCtl, focusNode: _nickFocus, - onTap: () => _kickFocus(_nickFocus), textInputAction: TextInputAction.next, autocorrect: false, enableSuggestions: false, @@ -1404,7 +1402,6 @@ class _ConnectFormState extends State<_ConnectForm> { TextField( controller: widget.passwordCtl, focusNode: _passwordFocus, - onTap: () => _kickFocus(_passwordFocus), obscureText: true, textInputAction: TextInputAction.done, autocorrect: false,