Day 5 - 令人煩躁的按鈕事件傳遞

開發 UI 畫面的過程中,常見的功能開發情境有很多,除了顯示資訊以外,另外一個常見的情境是提供一個按鈕,讓使用者執行特定功能了。在 Flutter 中,如果要製作一個 UI 讓使用者可以點擊,我們通常會使用 GestureDetector 並在其 onTap 參數中給定執行方法。
跨層傳遞 Callback
在經歷過前幾天討論的主題,有時候我們會發現,經過不斷地重構,最終按鈕事件是在子 Widget 中綁定。在下面的例子中,GameItemView 是列表中的每一個 Item,而當中包含一個購買按鈕。為了讓按鈕能正常工作,我們把 buyGame 方法當作參數,一路向子 Widget 傳,直到購買按鈕裡頭的 GestureDetetor。


這種做法十分簡單,也十分易懂。但是隨著 Widget 越抽越多層,Callback 方法需要透過參數,一路往子層傳遞,使用起來就不是這麼方便。而且中間層的 GameItemView,自己不需要這個方法,卻被迫開出這個 onTap 參數,只為了把他傳遞給 GamePurchaseButton。
利用 BuildContext 取得執行方法
為了解決這個問題,讓我們來嘗試另外一個作法,我們利用 BuildContext 來取得 GameListScreen,並直接執行其身上的 buyGame 方法。


透過這個方式,我們可以消除掉中間 Widget 身上的 onTap。當使用者按下按鈕,嘗試執行方法時,子 Widget 就會透過 BuildContext 尋找目標 Widget,然後呼叫其身上的方法。
沒有完美的解法
這個方式十分方便,消除了中間 Widget 的多餘參數,但是這個做法也帶來了比較明顯的缺點:子 Widget 直接依賴了目標 Widget,造成子 Widget 比較難以被重複使用。以上面的例子來說,假設今天有一個 Other 也同樣使用了 GameItemView,當 GamePurchaseButton 被按下時,由於找不到 GameListScreen,所以按鈕無法正常運作。

因為祖先 Widget 中不存在 GameListScreen,context.findAncestorWidgetOfExactType 時就會找回 null,我們也必須多檢查 null,避免不預期的錯誤。
結論
如果執行方法的 Widget 與擁有方法的 Widget 距離並不是太遠,使用參數傳遞可能是比較合適的方式。如果真的發現參數需要傳遞太多層,或許可以思考是否 Widget 拆了太多層了,可以適時考慮合併。雖然 context.findAncestorWidgetOfExactType 可以解決 onTap 傳太多層的問題,但也讓 Widget 難以重用,如非必要,還是優先考慮傳遞參數為主,畢竟傳遞參數的一大優點就是能清晰的呈現意圖。
參考
- GestureDetector:https://www.youtube.com/watch?v=WhVXkCFPmK4
- findAncestorWidgetOfExactType: https://api.flutter.dev/flutter/widgets/BuildContext/findAncestorWidgetOfExactType.html
Paul's Learning