======================================
「能正常跑的程式碼就盡量不要動」,這句話再一次證明了它的立場,實際上確實不少程式設計師都有「程式碼潔癖」,喜歡對程式碼進行「清洗」和「重構」,但優雅的背後,很多時候也伴隨著看不見的坑在等著你。
你以為上一代的人為什麼就偏要寫得「那麼蠢」?
回到正題,近日不少人在 #180842 中反饋,當 Flutter 升級後,點擊帶有 AutoFill 的輸入框時,鍵盤會像「抽風」一樣先彈起、收縮、再彈起:
這個乍看似乎沒什麼問題,但如果在 Flutter 3.38.7 的版本裡,它的表現是這樣的:
這麼一對比就可以很直觀看出來,這是一個負優化,特別是對於剛升級的使用者來說,這就顯得很莫名其妙:

而從目前的情況來看,問題的來源是以下這個 PR ,它是用來修復 autofill 上下文清理的提交,在這個 commit 裡,對 FlutterTextInputPlugin.mm 進行了重構,主要是簡化輸入視圖的生命週期管理:

但是在這個過程裡,它修改了 removeFromSuperview 的呼叫時機,看起來合理,是在清理無用 view,但問題在於呼叫順序:

在多輸入框切換(A → B)時,Flutter 平台端的呼叫順序通常是:
TextInput.clearClient (A)
TextInput.setClient (B)
也就是說:
但是如果在 clear 階段就把承載輸入的 _activeView 從 view hierarchy 裡 remove,那麼
becomeFirstResponder對比 3.38 時,當時採用的是「懶惰清理」策略,即使輸入 client 關閉,底層的原生視圖(activeView)依然掛載在視圖樹上。
而 3.41 一旦 clearTextInputClient 被呼叫,就會立即執行 removeFromSuperview,因為:
activeView 刪除了而這個情況,在 iOS 18+ 之後的輸入堆疊 + AutoFill / 密碼建議 UI 更敏感:
實際上上述的修改,如果不是事後看會覺得有問題,在體驗與邏輯上,當下可能都不覺得有什麼問題。
而這就是導致鍵盤在螢幕內上下閃動的原因,實際上類似問題在 React Native 上也曾出現過:

因為無論是 Flutter 或 RN,它們都不是「原生即時渲染」,而是透過一個中間層與 iOS 通訊,所以上述的 RN 也有過類似的鍵盤高度計算等問題,不過 RN 上主要是 AutoFill UI + layout 的競態導致的問題。
而針對這個問題,造成問題的原作者也提交了新的修復 PR #182661,在 FlutterTextInputPlugin.mm 引入了全新狀態,不再直接 removeFromSuperview,而是標記一個 _pendingInputViewRemoval = YES:

PR 的目的是將真正的移除動作放到
hideTextInput階段,也就是確保resignFirstResponder已經完成,從而對齊原本的系統節奏。
可以看到,這原本也不算是什麼大改動,出發點也是良善的,但這種細節的邊界情況,往往也是造成大問題的導火線,這種 Bug 對使用者來說,雖然不影響實際功能,但在體驗上確實是致命缺陷。
所以很多時候,你可能會想為什麼一些簡單的修改,Flutter 花很久才合併或提交,這就是一個典型例子,誰能保證 feature 有被完整回歸?說人話就是:我要不要為這東西的未來背鍋?
所以,每個歷史遺留的爛程式碼,大多都有它存在的理由,單純因為爛就把它砍掉的也有,但更多時候,大家還是傾向在爛上雕花,除非這坨當初就是自己拉的,你還知道它臭在哪裡。