Android 預測返回(Predictive Back)
摘要
#為了支援 Android 14 的預測返回(Predictive Back)功能, 一組預先(ahead-of-time)API 已取代即時(just-in-time)導覽 API, 例如 WillPopScope 和 Navigator.willPop。
背景
#Android 14 推出了 預測返回(Predictive Back)功能, 讓使用者在進行有效的返回手勢時,可以預覽當前 Route 背後的內容,並決定是否繼續返回或取消該手勢。這與 Flutter 允許開發者在收到返回手勢後才取消返回的導覽 API 不相容。
在預測返回(Predictive Back)中,當使用者啟動返回手勢時,返回動畫會立即開始,且在動畫被提交前就已經執行。此時 Flutter 應用程式無法決定是否允許這個操作,必須事先(ahead-of-time)就知道結果。
因此,所有允許 Flutter 應用程式開發者在收到返回手勢時才取消返回導覽的 API 現已被棄用。這些 API 已被等效的 API 取代,這些新的 API 會隨時維護一個布林值(boolean state),用來決定是否允許返回導覽。若允許,預測返回動畫會如常發生;否則,導覽會被阻止。在這兩種情況下,應用程式開發者都會被通知有返回操作被嘗試,以及該操作是否成功。
PopScope
#PopScope 類別直接取代了 WillPopScope,以支援預測返回(Predictive Back)。不再是在發生時決定是否允許 pop,而是預先透過 canPop 布林值設定。你仍然可以透過 onPopInvoked 來監聽 pop 事件。
PopScope(
canPop: _myPopDisableEnableLogic(),
onPopInvoked: (bool didPop) {
// Handle the pop. If `didPop` is false, it was blocked.
},
)Form.canPop 與 Form.onPopInvoked
#這兩個新參數是基於 PopScope,並取代了已棄用的 Form.onWillPop 參數。它們與 PopScope 的使用方式與上方所述相同。
Form(
canPop: _myPopDisableEnableLogic(),
onPopInvoked: (bool didPop) {
// Handle the pop. If `didPop` is false, it was blocked.
},
)Route.popDisposition
#這個 getter 會同步回傳該 Route 的 RoutePopDisposition,用來描述 pop 操作的行為方式。
if (myRoute.popDisposition == RoutePopDisposition.doNotPop) {
// Back gestures are disabled.
}ModalRoute.registerPopEntry 和 ModalRoute.unregisterPopEntry
#使用這些方法來註冊PopScope元件(Widgets),當 Route 判斷是否可以 pop 時,這些元件會被評估。這項功能可用於實作自訂的PopScope元件(Widget)。
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
if (nextRoute != _route) {
_route?.unregisterPopEntry(this);
_route = nextRoute;
_route?.registerPopEntry(this);
}
}遷移指南
#從 WillPopScope 遷移至 PopScope
#WillPopScope 元件(Widget)的直接替代方案是 PopScope 元件(Widget)。 在許多情況下,原本在 onWillPop 中於返回手勢(back gesture)時執行的邏輯, 可以在建構(build)階段完成,並設置給 canPop。
遷移前的程式碼:
WillPopScope(
onWillPop: () async {
return _myCondition;
},
child: ...
),遷移後的程式碼:
PopScope(
canPop: _myCondition,
child: ...
),在需要獲得 pop 嘗試通知的情況下,可以使用 onPopInvoked 方法,其用法與 onWillPop 類似。
請注意,onWillPop 會在 pop 被處理之前呼叫,並且有能力取消該操作,而 onPopInvoked 則是在 pop 完成處理後才會被呼叫。
遷移前的程式碼:
WillPopScope(
onWillPop: () async {
_myHandleOnPopMethod();
return true;
},
child: ...
),遷移後的程式碼:
PopScope(
canPop: true,
onPopInvoked: (bool didPop) {
_myHandleOnPopMethod();
},
child: ...
),從 WillPopScope 遷移到 NavigatorPopHandler 以支援巢狀 Navigator
#WillPopScope 的一個非常常見的使用情境,是在使用巢狀 Navigator 元件(Widgets)時,正確處理返回手勢(back gestures)。 雖然也可以使用 PopScope 來實現這個需求, 但現在有一個包裝元件(wrapper widget)可以讓這件事變得更簡單: NavigatorPopHandler。
遷移前的程式碼:
WillPopScope(
onWillPop: () async => !(await _nestedNavigatorKey.currentState!.maybePop()),
child: Navigator(
key: _nestedNavigatorKey,
…
),
)遷移後的程式碼:
NavigatorPopHandler(
onPop: () => _nestedNavigatorKey.currentState!.pop(),
child: Navigator(
key: _nestedNavigatorKey,
…
),
)從 Form.onWillPop 遷移至 Form.canPop 和 Form.onPopInvoked
#過去,Form 在底層使用了一個 WillPopScope 實例,並對外暴露其 onWillPop 方法。 現在已經改為使用 PopScope,並對外暴露其 canPop 和 onPopInvoked 方法。 遷移方式與上方所述從 WillPopScope 遷移至 PopScope 完全相同。
從 Route.willPop 遷移至 Route.popDisposition
#Route 的 willPop 方法過去會回傳一個 Future<RoutePopDisposition>, 以因應彈出操作可能會被取消的情況。由於現在不再需要這樣的處理, 相關邏輯已簡化為同步 getter。
遷移前的程式碼:
if (await myRoute.willPop() == RoutePopDisposition.doNotPop) {
...
}遷移後的程式碼:
if (myRoute.popDisposition == RoutePopDisposition.doNotPop) {
...
}從 ModalRoute.add/removeScopedWillPopCallback 遷移至 ModalRoute.(un)registerPopEntry
#在內部,ModalRoute 會透過將 WillPopScope 註冊到 addScopedWillPopCallback 和 removeScopedWillPopCallback,來追蹤其元件(Widget)子樹中 WillPopScope 的存在情況。 由於 PopScope 取代了 WillPopScope,這些方法也分別被 registerPopEntry 和 unregisterPopEntry 取代。
PopEntry 由 PopScope 實作,以僅向 ModalRoute 暴露必要的最少資訊。任何自行撰寫 PopScope 的開發者,都應該實作 PopEntry,並將其元件向所屬的 ModalRoute 註冊與取消註冊。
遷移前的程式碼:
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (widget.onWillPop != null) {
_route?.removeScopedWillPopCallback(widget.onWillPop!);
}
_route = ModalRoute.of(context);
if (widget.onWillPop != null) {
_route?.addScopedWillPopCallback(widget.onWillPop!);
}
}遷移後的程式碼:
@override
void didChangeDependencies() {
super.didChangeDependencies();
_route?.unregisterPopEntry(this);
_route = ModalRoute.of(context);
_route?.registerPopEntry(this);
}從 ModalRoute.hasScopedWillPopCallback 遷移至 ModalRoute.popDisposition
#這個方法先前主要用於 Cupertino 函式庫中,處理與 Predictive Back 類似的使用情境,也就是某些返回(back)轉場允許取消導覽(navigation)。當存在任何可能由WillPopScope元件(Widget)取消 pop 的情況時,該 Route 的轉場就會被停用。
現在,由於 API 要求必須預先決定這個行為,因此不再需要僅根據PopScope元件(Widget)的存在來推測。判斷ModalRoute是否因PopScope元件(Widget)而被阻擋 pop 的最終邏輯,已經內建於ModalRoute.popDisposition中。
遷移前的程式碼如下:
if (_route.hasScopedWillPopCallback) {
// Disable predictive route transitions.
}遷移後的程式碼:
if (_route.popDisposition == RoutePopDisposition.doNotPop) {
// Disable predictive route transitions.
}遷移 back 確認對話框
#WillPopScope 有時會用來在收到返回手勢(back gesture)時顯示確認對話框。
這仍然可以透過 PopScope 以類似的方式實現。
遷移前的程式碼:
WillPopScope(
onWillPop: () async {
final bool? shouldPop = await _showBackDialog();
return shouldPop ?? false;
},
child: child,
)遷移後的程式碼:
return PopScope(
canPop: false,
onPopInvoked: (bool didPop) async {
if (didPop) {
return;
}
final NavigatorState navigator = Navigator.of(context);
final bool? shouldPop = await _showBackDialog();
if (shouldPop ?? false) {
navigator.pop();
}
},
child: child,
)支援預測返回(predictive back)
#- 執行 Android 14(API 等級 34)或以上版本。
- 在裝置的「開發人員選項」中啟用預測返回(predictive back)功能旗標。 在未來的 Android 版本中將不再需要這個步驟。
- 在
android/app/src/main/AndroidManifest.xml中設定android:enableOnBackInvokedCallback="true"。 如有需要,請參考 Android 的完整指南 以協助 Android 應用程式遷移並支援預測返回。 - 請確保你使用的是 Flutter 版本
3.14.0-7.0.pre或更高版本。 - 請確認你的 Flutter 應用程式沒有使用
WillPopScope元件(Widget)。使用該元件會停用 預測返回。若有需要,請改用PopScope。 - 執行應用程式,並從螢幕左側滑動進行返回手勢。
時程
#合併於版本:3.14.0-7.0.pre
穩定版釋出:3.16
參考資料
#API 文件:
PopScopeNavigatorPopHandlerPopEntryForm.canPopForm.onPopInvokedRoute.popDispositionModalRoute.registerPopEntryModalRoute.unregisterPopEntry
相關議題:
相關 PR: