# 使用WidgetsBindingObserver拦截

WidgetsBindingObserver我们经常看到的是用它来做Android生命周期事件的监听,无意中发现它也可以拦截android返回按键事件,并且这个拦截还是全局的。

import 'package:flutter/material.dart';
class DidPopRouteDmeo extends StatefulWidget {
  
  _DidPopRouteDmeoState createState() => _DidPopRouteDmeoState();
}
class _DidPopRouteDmeoState extends State<DidPopRouteDmeo> with WidgetsBindingObserver {
  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }
  
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
  
  Future<bool> didPopRoute() async {
    return true;
  }
  
  Widget build(BuildContext context) {
    return MaterialApp(title: '', home: HomePage());
  }
}
class HomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        print("HomePage");
        return false;
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text("HomePage"),
        ),
        body: Container(
          alignment: Alignment.center,
          child: RaisedButton(
            child: Text("第二屏"),
            onPressed: () {
              Navigator.push(context,
                  MaterialPageRoute(builder: (context) => SecondPage()));
            },
          ),
        ),
      ),
    );
  }
}
class SecondPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // 当点击菜单栏返回按钮时该页面退出了,说明只能拦截返回按键
    return WillPopScope(
      onWillPop: () async {
        print("SecondPage");
        return true;
      },
      child: Scaffold(
          appBar: AppBar(title: Text("Second Page")), body: Container()),
    );
  }
}
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
62
63
64
65
66
67
68
69
70
71

上面代码演示了通过WidgetsBindingObserver拦截所有页面的返回按键。

注意:如果我们使用了MaterialApp,WidgetsApp时,我们必须包裹它们才行,就是上面代码实现的样子。

# 为什么要包裹MaterialApp,WidgetsApp

上面例子中,我们重写了WidgetsBindingObserver.didPopRoute(),并返回true, 在initState()中通过WidgetsBinding.instance.addObserver(this);将当前Widget添加到了WidgetsBinding中,当有对应的事件发生时会触发当前Widget的对应事件,也就是我们重写的didPopRoute()方法。

WidgetsBinding源码

mixin WidgetsBinding{
   // 观察者队列
   final List<WidgetsBindingObserver> _observers = <WidgetsBindingObserver>[];
   // 添加一个观察者
   void addObserver(WidgetsBindingObserver observer) => _observers.add(observer);
   // 循环调用所有观察者
   
  Future<void> handlePopRoute() async {
    for (WidgetsBindingObserver observer in List<WidgetsBindingObserver>.from(_observers)) {
      // 当一个观察者返回true时,则后面的观察者就不能被调用了
      if (await observer.didPopRoute())
        return;
    }
    SystemNavigator.pop();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面代码我们可以看到,当有多个WidgetsBindingObserver被注册进队列时,WidgetsBinding收到返回按钮事件时会通知被注册的观察者,但是当其中一个返回true时,后面的观察者就不能被通知,返回false时则调用SystemNavigator.pop()终结该页面,当是最后一个页面时app也会退出。

进一步分析WidgetsBinding._observers

上面的例子中,当我们的程序启动时,观擦者队列是下面这样的:

[_DidPopRouteDmeoState,_WidgetsAppState,_MediaQueryFromWindowsState]

因为_WidgetsAppState是在_MediaQueryFromWindowsState之上构建的,而且WidgetsAppState并没有暴漏任何接口可以让我们的_DidPopRouteDmeoState插入到它们之间,所以_DidPopRouteDmeoState只能放在它们之前或者之后的位置。

放在之后的位置时

由于_WidgetsAppState重写了didPopRoute,当返回true时就return了,而返回false时又直接SystemNavigator.pop()了,导致后面的WidgetsBindingObserver根本没有机会执行,所以在_WidgetsAppState的子元素中重写WidgetsBindingObserver没有意义,因为它没有机会被调用。

放在之前的位置时

如果返回true会导致后面的观察者都不能执行,所以就可以做到所有页面禁用返回按钮,但有另外的问题,并不是所有的页面都想禁用返回按钮,这个问题在后面介绍。

# WidgetsBindingObserver总结

WidgetsBindingObserver可以拦截所有页面的物理按键。

由于flutter的路由功能是在_WidgetsAppState中实现的,而MaterialApp又是包裹的WidgetsApp,所以我们只能选择包裹MaterialApp来做到对WidgetsBindingObserver的监听。