# 使用WillPopscope实现返回拦截总结

如果你搜索flutter实现返回拦截的方法,WillPopscope是被介绍最多的一个,因为它是**最简单也是最容易被搜索到的一种拦截方式,同时它也是最繁琐**的一种实现方式。

# 为什么简单,容易在哪里

下面几行代码就实现了拦截功能,每当我们点击返回按钮时都会都到拦截通知,你在onWillPop中选择行的返回true/false,来做出响应:

  1. 当返回false时,会阻止返回
  2. 当返回true时,会正常的返回。
import 'package:flutter/material.dart';
class WillPopScopeDemo extends StatefulWidget {
  
  _WillPopScopeDemoState createState() => _WillPopScopeDemoState();
}
class _WillPopScopeDemoState extends State<WillPopScopeDemo> {
  int clickTimes=1;
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "拦截返回键",
      home: WillPopScope(
        onWillPop: () async{
          setState(() {});
          return false;
        },
        child: Scaffold(
          appBar: AppBar(title: Text("Home")),
          body: Container(
            alignment: Alignment.center,
            child: Text("点击返回按钮次数:${clickTimes++}"),
          ),
        ),
      ),
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

上面代码只有一个页面所以不太容易看出来效果,下面做个有多个页面的演示。

# 多页面效果演示

import 'package:flutter/material.dart';
class WillPopScopeDemo2 extends StatefulWidget {
  
  _WillPopScopeDemo2State createState() => _WillPopScopeDemo2State();
}
class _WillPopScopeDemo2State extends State<WillPopScopeDemo2> {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "拦截返回键",
      home: 
        // 使用Builder获取context可以保证Navigator.push有效
        Builder(
        builder: (context) => WillPopScope(
          onWillPop: () async {
            print("第一屏返回按钮被拦截");
            return false;
          },
          child: Scaffold(
            appBar: AppBar(title: Text("Home")),
            body: Container(
              alignment: Alignment.center,
              child: RaisedButton(
                child: Text("第二屏"),
                onPressed: () {
                  Navigator.push(context,
                      MaterialPageRoute(builder: (context) => SecondsPage()));
                },
              ),
            ),
          ),
        ),
      ),
    );
  }
}
// 第二屏
class SecondsPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        print("第二屏返回按钮被拦截");
        return false;
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text("第二屏"),
        ),
        body: Container(
          alignment: Alignment.center,
          child: Text("第二屏"),
        ),
      ),
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

上面例子中当我们切换到第二屏时,物理你点击物理返回按键,还是AppBar上的返回箭头,页面都不会返回,这是因为我们返回false时,阻止了页面的返回。

这里需要注意一点,AppBar上的箭头调用的是Navitagory.maybePop()

# 为什么最繁琐

只有被WillPopScope包裹的widget才会收到拦截通知,所以如果我们有多个页面需要处理拦截就需要买个页面的包裹在WillPopScope中。

从多页面效果演示的例子,我们可以看到,在主页面与第二屏我们都使用WillPopScope做了拦截,你可以去掉第一屏中WillPopScope或者将返回值改为ture,看一下执行效果,结论是:app会退出运行。

# 下面代码证明了app会退出:

ModalRoute源码

abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> { 
 	...
 
  Future<RoutePopDisposition> willPop() async {
    final _ModalScopeState<T> scope = _scopeKey.currentState;
    assert(scope != null);
    for (WillPopCallback callback in List<WillPopCallback>.from(_willPopCallbacks)) {
      //这里调用也就是我们通过WillPopScope添加的onWillPop方法,如果我们返回false就不执行pop,所以页面不会退出
      if (!await callback())
        return RoutePopDisposition.doNotPop;
    }
    return await super.willPop();
  }
  	...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

调用路径:Navigator.maybePop()->ModalRoute.willPop()->WillPopCall.onWillPop

这里面会有两种情况来触发Navigator.maybePop()的调用:

  1. AppBar上的返回箭头触发

  2. Android返回按键触发

# 总结

WillPopscope可以拦截具体某个页面的返回动作,只要我们使用WillPopscope包裹该页面并实现onWillPop()方法就行。

无论是点击AppBar上的返回按钮,还是点击Android的物理返回按钮触发的都是Navigator.maybePop(),从而触发WillPopscope.onWillPop()