怎樣使用 Flutter 中的 WillPopScope Widget

zonble
8 min readFeb 12, 2020

--

WillPopScope 這個 Widget 的用途是處理各種「是否應該離開目前所在畫面」的行為。

一套 Flutter app 當中,可能有多種不同的方式,可以讓用戶離開目前所在畫面,像是:如果目前的畫面上有個 App Bar 或是 CupertinoNavigationBar 的話,那麼在這個上方的 Bar 往往就會有個後退按鈕;在 Android 平台上,用戶可以透過實體的後退按鈕回到前一頁;我們也可以將 Flutter 輸出成 Web app,瀏覽器當中也有後退按鈕。雖然後退方式有很多種,但我們都可以用 WillPopScope 處理。

WillPopScope 大概算是 iOS 工程師來寫 Flutter 的時候,最容易忘記要去使用的 Widget。或是說,iOS 工程師,往往會忽略用戶在其他平台上可以怎樣退出。

你在以下幾種狀況,就可能會用到 WillPopScope。

  1. 你需要向用戶確認是否要退出。
  2. 你的 App 當中有多個 Navigator,你想要的是讓其中一個 Navigator 退出,而不是直接讓在 Widget tree 底層的 Navigator 退出。

我在這裡用 Flutter Web 做了一個簡單的範例,可以體驗看看各種使用 WillPopScope 的情境。 https://zonble.github.io/willpopscope_demo

詢問用戶是否要退出

如果你的 Flutter app 是一個 web app,當用戶透過瀏覽器的後退按鈕回到前一頁,往往就會遺棄目前頁面上的狀態;在 Android app 中,如果在 App 最一開始的畫面上按下後退,預設會關閉目前的 activty 回到桌面,用戶再回到 app 裡頭,就會建立新的 activity,也會讓用戶遺失狀態。

如果你不打算做一些自動儲存、回復的機制,而用戶在你的 App 中正在輸入大量資料,狀態一旦一不小心按到退出而遺失,就等於讓用戶做白工,辛辛苦苦輸入的資料不見,是一種糟糕到了極點的使用者體驗。

我們可以用 WillPopScope,在用戶按到退出的時候,詢問是否真的要退出。作法大概有幾種,你可以在用戶按到後退按鈕時跳出一個對話框,用戶按下「確認」才真的退出:或是,你可以用 Toast 或是 Snackbar 告訴用戶,請多按一次後退按鈕,才會真的退出。

WillPopScope 裡頭有一個屬性叫做 onWillPop,是一個應該要回傳帶 bool 的 future 的 function,當這個 function 回傳 true 的 future 的時候,才代表我們應該要退出。假如我們想要跳出對話框,在這邊就呼叫 showDialog:

WillPopScope(
onWillPop: () async => showDialog(
context: context,
builder: (context) =>
AlertDialog(title: Text('你確定要退出嗎?'), actions: <Widget>[
RaisedButton(
child: Text('退出'),
onPressed: () => Navigator.of(context).pop(true)),
RaisedButton(
child: Text('取消'),
onPressed: () => Navigator.of(context).pop(false)),
])),
child: ....
)

效果如下:

而如果我們想要用戶連按兩下後退才真的退出,可以在我們的 StatefulWidget 的 State 當中,用一個成員變數記住是不是已經按過一次後退。

var _snackBarPresenting = false;

如果這個變數是 true,那麼,就代表已經按過一次後退,這時候才真的退出目前頁面。

WillPopScope(
onWillPop: () async {
if (_snackBarPresenting) return true;
_snackBarPresenting = true;
var snackBar = SnackBar(content: Text('再按一次 Back 按鈕退出'));
Scaffold.of(context).showSnackBar(snackBar)
..closed.then((_) => _snackBarPresenting = false);
return false;
},
child:...
)

畫面中有自訂的 Navigator

我們的 Widget 通常會位在 MaterialApp 或是 CupertinoApp 這些 Widget 之下。MaterialApp 與 CupertinoApp 本身就有一個 Navigator,我們一般在 push route 的時候,使用的就是這個 Navigator。不過,在某些狀況下,我們就可能會自己定義新的 Navigator 出來,通常會是在一些需要分割畫面的時候,像是:

  • 我們希望在 App 畫面中有一個長駐的 bar,所以用 Column 將畫面分成上下兩塊,一塊是常駐的 Bar,另一塊則是主要瀏覽區域。在這個主要瀏覽區域當中,就寫了一個自己的 Navigator。
  • 我們用到了像是 TabView、BottomNavigationBar、CupertinoTabView 等用來切割畫面用的 Widget,像是,我們希望一個 App 當中有多個 Tab,然後每個 Tab 裡頭都有自己的瀏覽行為,所以每個 Tab 當中其實就都有各自的 Navigator。

由於我們需要知道在 Widget tree 下方的 Navigator 的狀態,所以我們可以透過注入一把 Global Key,拿到 Navigator 當中的 Navigator State。我們可以向 Navigator State 詢問 canPop(),意思是這個 Navigator 是不是已經 push 了一些 route,而且可以往上後退。我們可以來看一下這段 code:

class _DemoState extends State<Demo3> {
GlobalKey<NavigatorState> _key = GlobalKey();
@override
Widget build(BuildContext context) => Scaffold(
body: WillPopScope(
onWillPop: () async {
if (_key.currentState.canPop()) {
_key.currentState.pop();
return false;
}
return true;
},
child: Column(
children: <Widget>[
Expanded(
child: Navigator(
key: _key,
initialRoute: '',
onGenerateRoute: (settings) => CupertinoPageRoute(
builder: (context) => _Demo3Inner(depth: 0),
settings: RouteSettings(isInitialRoute: true)))),
Container(
height: 44 + MediaQuery.of(context).padding.bottom,
width: double.infinity,
color: Colors.black12,
child: Column(children: <Widget>[
SizedBox(height: 10),
Text('Bottom Bar')
]))
],
)));
}

這個畫面中,用 Column 分成了上下兩塊,下方是一個常駐的 bar,上方是一個 Navigator,在建立了時候,我們把 key 送了進去。那麼,在 WillPopScope 的 onWillPop 中,如果 canPop() 為 true,我們就向 Navigator State 呼叫 pop(),並且回傳 false,退出這個 Navigator 當中的頁面;如果是 false 的話,那就是裡頭這個 Navigator 已經在初始頁面,我們就可以回傳 true,退出整頁 — 如果是 mobile app 的話,那就代表我們要退出目前的 app 回到桌面,關閉目前的 activity。

如果像是在 CupertinoApp 當中,使用 CupertinoTabView 的話,那麼,由於每個 Tab 往往都有自己的 Navigator,所以我們就會對每個 Navigator 都注入一把 Global Key。

--

--

zonble
zonble

Written by zonble

XDDDD - eXtreme Due Date Driven Development

Responses (1)