如何在 Navigator.of(context).push(....) 之后临时取消订阅 Stream?

Posted

技术标签:

【中文标题】如何在 Navigator.of(context).push(....) 之后临时取消订阅 Stream?【英文标题】:How to temporarily unsubscribe to Stream after Navigator.of(context).push(....)? 【发布时间】:2020-06-05 02:44:48 【问题描述】:

场景:有两个页面。 PhonePageOtpPage 。用户在PhonePage 中输入电话号码并被重定向到OtpPage 以验证发送给他的OTP。

问题:与服务器对话的 API 使用 StreamController.broadcast() 告诉应用程序响应请求。此流由PhonePageOtpPage 共享并产生事件。两个页面监听流并根据事件决定做什么。

但是,Navigator.push() 之后,旧页面仍在监听流。因此,当OtpPage 中的用户点击重新发送按钮时,PhonePage 中的Navigator.push 仍会被调用,尽管它不应该调用。

问题 Flutter 必须如何处理这种情况?我试过onDispose(),但它没有被调用。 如果您也能解释为什么也没有调用 onDispose,我将不胜感激。

代码:这是重现场景的代码。您可以将其粘贴到您的 IDE 或 DartPad https://dartpad.dev/flutter (注​​意:当您转到 OtpPage 并在文本字段中添加一些文本时,单击重新发送按钮。注意如何在顶部添加一个新的 OtpPage 小部件导航树。这是不受欢迎的行为)

import 'dart:async';

import 'package:flutter/material.dart';

final fakeApiResponse = StreamController.broadcast();

void main() => runApp(MyApp(),);

class MyApp extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: PhoneNumber(),
    );
  


class PhoneNumber extends StatefulWidget 
  @override
  _PhoneNumberState createState() => _PhoneNumberState();


class _PhoneNumberState extends State<PhoneNumber> 
  StreamSubscription apiEventListner;

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Text('Enter your phone number'),
          RaisedButton(
            child: Text('Send OTP'),
            onPressed: () 
              fakeApiResponse.add('OTP Sent');
            ,
          ),
        ],
      ),
    );
  

  @override
  void initState() 
    super.initState();
    apiEventListner = fakeApiResponse.stream.listen((data) 
      if (data == 'OTP Sent') 
        Navigator.of(context).push(
          MaterialPageRoute(
            builder: (context) => VerifyOtp(),
          ),
        );
      
    );
  

  @override
  void dispose() 
    super.dispose();
    apiEventListner.cancel();
  


class VerifyOtp extends StatefulWidget 
  @override
  _VerifyOtpState createState() => _VerifyOtpState();


class _VerifyOtpState extends State<VerifyOtp> 
  StreamSubscription apiEventListner;


  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          TextField(
            decoration: InputDecoration(hintText: 'Enter OTP Here'),
          ),
          RaisedButton(
            child: Text("Verify"),
            onPressed: () 
              fakeApiResponse.add('OTP Verified');
            ,
          ),
          RaisedButton(
            child: Text("Didn't get the code? Resend OTP"),
            onPressed: () 
              fakeApiResponse.add('OTP Sent');
            ,
          ),
        ],
      ),
    );
  

  @override
  void initState() 
    super.initState();
    apiEventListner = fakeApiResponse.stream.listen((data) 
      if (data == 'OTP Sent') 
        // show the dialog
        showDialog(
          context: context,
          builder: (BuildContext context) 
          return  AlertDialog(
              title: Text("OTP Resent"),
              content: Text("Enter new OTP"),

            );
          ,
        );
      else if (data == 'OTP Verified')
        Navigator.of(context).push(MaterialPageRoute(builder: (context)=>SuccessPage()));
      
    );
  

  @override
  void dispose() 
    super.dispose();
    apiEventListner.cancel();
  


class SuccessPage extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: Center(
        child: Text('SUCCESS!'),
      ),
    );
  

【问题讨论】:

我无权访问 Api 代码,因为它是一个库,因此我无法控制响应事件名称。 Api 只知道一件事:Send Otp 将响应 Otp Sent。它没有重新发送 Otp 的概念。您只需再次调用 Send Otp 即可重新发送它 【参考方案1】:

我找到了解决方案。无论如何我都会在这里发布它,以防它可能对其他人有帮助。

诀窍是检测当前活动路由,如果当前小部件不是 Streams 侦听器内的当前路由,则返回。

Flutter 有一个名为 ModalRoute route = ModalRoute.of(context); 的 API,如果当前小部件是当前路由,route.isCurrent 将为真。

然后您必须在两个页面中添加此检查。

最终的工作代码是:

import 'dart:async';

import 'package:flutter/material.dart';

final fakeApiResponse = StreamController.broadcast();

void main() => runApp(
      MyApp(),
    );

class MyApp extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: PhoneNumber(),
    );
  


class PhoneNumber extends StatefulWidget 
  @override
  _PhoneNumberState createState() => _PhoneNumberState();


class _PhoneNumberState extends State<PhoneNumber> 
  StreamSubscription apiEventListner;

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Text('Enter your phone number'),
          RaisedButton(
            child: Text('Send OTP'),
            onPressed: () 
              fakeApiResponse.add('OTP Sent');
            ,
          ),
        ],
      ),
    );
  

  @override
  void initState() 
    super.initState();
    apiEventListner = fakeApiResponse.stream.listen((data) 
      ModalRoute route = ModalRoute.of(context);
      String name = route?.settings?.name;
      print("Phone page isCurrent: $route?.isCurrent isFirst: $route?.isFirst active: $route?.isActive $name");
      if (route?.isCurrent != null && !route.isCurrent) 
        return;
      
      if (data == 'OTP Sent') 
        Navigator.of(context).push(
          MaterialPageRoute(
            builder: (context) => VerifyOtp(),
          ),
        );
      
    );
  

  @override
  void dispose() 
    super.dispose();
    apiEventListner.cancel();
  


class VerifyOtp extends StatefulWidget 
  @override
  _VerifyOtpState createState() => _VerifyOtpState();


class _VerifyOtpState extends State<VerifyOtp> 
  StreamSubscription apiEventListner;

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          TextField(
            decoration: InputDecoration(hintText: 'Enter OTP Here'),
          ),
          RaisedButton(
            child: Text("Verify"),
            onPressed: () 
              fakeApiResponse.add('OTP Verified');
            ,
          ),
          RaisedButton(
            child: Text("Didn't get the code? Resend OTP"),
            onPressed: () 
              fakeApiResponse.add('OTP Sent');
            ,
          ),
        ],
      ),
    );
  

  @override
  void initState() 
    super.initState();
    apiEventListner = fakeApiResponse.stream.listen((data) 
      ModalRoute route = ModalRoute.of(context);
      String name = route?.settings?.name;
      print("OTP page isCurrent: $route?.isCurrent isFirst: $route?.isFirst active: $route?.isActive $name");
      if (route?.isCurrent != null && !route.isCurrent) 
        return;
      
      if (data == 'OTP Sent') 
        // show the dialog
        showDialog(
          context: context,
          builder: (BuildContext context) 
            return AlertDialog(
              title: Text("OTP Resent"),
              content: Text("Enter new OTP"),
            );
          ,
        );
       else if (data == 'OTP Verified') 
        Navigator.of(context).push(MaterialPageRoute(builder: (context) => SuccessPage()));
      
    );
  

  @override
  void dispose() 
    super.dispose();
    apiEventListner.cancel();
  


class SuccessPage extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: Center(
        child: Text('SUCCESS!'),
      ),
    );
  

【讨论】:

以上是关于如何在 Navigator.of(context).push(....) 之后临时取消订阅 Stream?的主要内容,如果未能解决你的问题,请参考以下文章

Flutter:Navigator.of(context).pop() 返回黑屏

Navigator.of(context, rootNavigator: true).push()中的`rootNavigator`有啥用?

Navigator.pop() - 如何传递 `context` 以供导航器读取 -

在showDialog中Flutter Navigator.of(context).pop(),在ios中关闭整个应用程序

查找已停用小部件的祖先是不安全的 => 使用 Riverpod => 使用 "Navigator.of(context).pushReplacementNamed('/page'

Flutter “孔雀开屏”的动画效果