Flutter - Provider 变量更改时触发导航

Posted

技术标签:

【中文标题】Flutter - Provider 变量更改时触发导航【英文标题】:Flutter - trigger navigation when Provider variable changes 【发布时间】:2020-04-13 17:58:47 【问题描述】:

我正在尝试在初始应用启动时显示启动画面,直到我正确检索到所有数据。检索由名为“ProductData”的类完成。一旦准备好,我想从启动页面导航到应用程序的主屏幕。

不幸的是,我找不到触发运行这种导航并侦听提供程序的方法的好方法。

这是我用来测试这个想法的代码。具体来说,我想运行命令 Navigator.pushNamed(context, 'home');当变量 shouldProceed 变为 true 时。不幸的是,下面的代码给了我错误,“在构建期间调用了 setState() 或 markNeedsBuild()。”

import 'package:catalogo/firebase/ProductData.dart';
import 'package:flutter/material.dart';=
import 'package:provider/provider.dart';

class RouteSplash extends StatefulWidget 
  @override
  _RouteSplashState createState() => _RouteSplashState();


class _RouteSplashState extends State<RouteSplash> 
  bool shouldProceed = false;

  @override
  Widget build(BuildContext context) 
    shouldProceed =
        Provider.of<ProductData>(context, listen: true).shouldProceed; 
    if (shouldProceed) 
      Navigator.pushNamed(context, 'home'); <-- The error occurs when this line is hit.
     else 
      return Scaffold(
        body: Center(
          child: CircularProgressIndicator(),
        ),
      );
    
  

有没有更好的方法来根据提供者的结果导航到页面?

【问题讨论】:

【参考方案1】:

如果您仍在等待数据,您应该显示加载初始屏幕,而不是尝试导航到新视图,并且一旦更改显示您的主主视图,如下所示:

import 'package:catalogo/firebase/ProductData.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Main extends StatefulWidget 
  @override
  _MainState createState() => _MainState();


class _MainState extends State<Main> 
  bool shouldProceed = Provider.of<ProductData>(context, listen: true).shouldProceed;

  @override
  Widget build(BuildContext context) 
    if(shouldProceed)
      return Home();
    else
      return RouteSplash();
    
  

【讨论】:

我已经见过这种方法几次了。令我烦恼的是,由于此场景中没有导航器,因此屏幕转换没有视觉效果。新屏幕突然弹出。这是接受还是放弃,还是可以改进? 好吧,你可以使用Navigator,但这会迫使你从Navigator堆栈中删除RouteSplash,因为你不希望你的用户能够导航回来到启动画面。 我处于与上述相同的情况:当我尝试推送由我的 BLoC 的状态更改触发的新屏幕时,我收到“在状态期间调用 setState 或 markNeedsBuild”。我发现的唯一方法是使用GlobalKey 来访问Navigator 状态,而不需要通常需要的BuildContext,并直接从BLoC 内部推送。对我来说,这也不是很干净。在我看来,导航应该留在应用程序的 UI 部分。还有其他方法吗? 今天偶然发现:github.com/felangel/bloc/issues/201... 不幸的是,大量的 BLoC 计数器教程使得很难找到真正重要的东西。感谢您让我继续寻找!【参考方案2】:

在本例中使用 BlocListener:

BlocListener(
    bloc: BlocProvider.of<DataBloc>(context),
    listener: (BuildContext context, DataState state) 
        if (state is Success)               
            Navigator.of(context).pushNamed('/details');
                      
    ,
    child: BlocBuilder(
        bloc: BlocProvider.of<DataBloc>(context),
        builder: (BuildContext context, DataState state)         
            if (state is Initial) 
                return Text('Press the Button');
            
            if (state is Loading) 
                return CircularProgressIndicator();
              
            if (state is Success) 
                return Text('Success');
              
            if (state is Failure) 
                return Text('Failure');
            
        ,
    
)

来源:https://github.com/felangel/bloc/issues/201

【讨论】:

【参考方案3】:

我想我有一个可以满足 OP 要求的解决方案。如果您将初始屏幕设为Stateful,则可以添加PostFrameCallback。这避免了在 build 运行时调用 Navigator 的任何问题。然后,您的回调可以调用Provider 需要读取数据的任何例程。这个读取数据例程可以传递一个包含Navigator 命令的进一步回调。

在我的解决方案中,我添加了进一步的回调,以便启动屏幕至少可见一秒钟(您可以在此处选择您认为合理的持续时间)。不幸的是,这会产生竞争条件,因此我需要导入 synchronized 包以避免出现问题。

import 'package:flutter/material.dart';
import 'package:reflect/utils/constants.dart';
import 'category_screen.dart';
import 'package:provider/provider.dart';
import 'package:reflect/data_models/app_prefs.dart';
import 'dart:async';
import 'dart:core';
import 'package:synchronized/synchronized.dart';

class LoadingScreen extends StatefulWidget 
  static const id = 'LoadingScreen';

  @override
  _LoadingScreenState createState() => _LoadingScreenState();


class _LoadingScreenState extends State<LoadingScreen> 
  bool readPrefsDone = false;
  bool timeFinished = false;
  Lock _lock = Lock();

  void initState() 
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) 
      Provider.of<AppPrefs>(context, listen: false).readPrefs(readDone);
      Timer(Duration(seconds: 1), () 
        timerDone();
      );
    );
  

  void timerDone() async 
    _lock.synchronized(() 
      if (readPrefsDone) 
        pushMainScreen();
      
      timeFinished = true;
    );
  

  void readDone() 
    _lock.synchronized(() 
      if (timeFinished) 
        pushMainScreen();
      
      readPrefsDone = true;
    );
  

  void pushMainScreen() 
    Navigator.pushReplacement(
      context,
      PageRouteBuilder(
        pageBuilder: (context, animation, animation2) => CategoryScreen(),
        transitionDuration: Duration(seconds: 1),
      ),
    );
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
        body: Container(
      color: Colors.white,
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Hero(
              tag: kFishLogoTag,
              child: Image(
                image: AssetImage('assets/fish_logo.png'),
              ),
            ),
            SizedBox(
              height: 30,
            ),
            Text(
              'Reflect',
              style: TextStyle(
                fontSize: 30,
                color: Color(0xFF0000cc),
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
      ),
    ));
  

【讨论】:

【参考方案4】:

遇到此问题的任何其他人都可以使用此代码

Future.delayed(Duration.zero, () => Navigate.toView(context));

这会导航到另一个屏幕而不会出现构建错误

【讨论】:

以上是关于Flutter - Provider 变量更改时触发导航的主要内容,如果未能解决你的问题,请参考以下文章

Flutter:如何在 Flutter 中使用 Switch 更改主题 - 我已经使用 Provider 实现了这个明暗主题,但不能使用 switch 更改

更改一个 Provider 中的属性会将另一个 Provider 中的属性更改为 List Flutter

使用上下文和观察更改模型时,Flutter Provider 不会触发重建

Flutter Provider - 在值更改时调用函数而不调用 build()

在 Flutter 中使用 Provider 更新和动画化 AnimatedList

Flutter GoogleMap Provider Context问题