【问题标题】:Flutter Provider nested navigationFlutter Provider 嵌套导航
【发布时间】:2020-04-21 21:28:52
【问题描述】:

我对提供程序和导航有疑问。

我有一个带有对象列表的HomeScreen。当您单击一个对象时,我会使用标签导航导航到 DetailScreen。这个DetailScreen 包裹着一个提供ViewModel 的ChangenotifierProvider

现在,当我使用 Navigator.of(context).push(EditScreen) 导航到另一个屏幕时,我无法访问 EditScreen 中的 ViewModel 抛出以下错误

════════ Exception caught by gesture ═══════════════════════════════════════════
The following ProviderNotFoundException was thrown while handling a gesture:
Error: Could not find the correct Provider<ViewModel> above this EditScreen Widget

这是我尝试实现的目标的简单概述

Home Screen
 - Detail Screen (wrapped with ChangeNotifierProvider)
   - Edit Screen
     - access provider from here

我知道问题出在哪里。我正在堆栈上推送一个新屏幕,并且更改通知器不再可用。 我考虑在我的应用程序之上创建一个详细信息存储库,其中包含 DetailView 的所有 ViewModel。

我知道我可以将 ChangeNotifier 包裹在我的 MaterialApp 周围,但我不想这样做,或者因为我不知道我需要哪个 Detail-ViewModel 而不能这样做。我想要列表中每个项目的 ViewModel

我真的不知道解决这个问题的最佳方法是什么。谢谢大家的帮助

这是一个简单的示例应用程序:

This is a picture of the image tree

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: RaisedButton(
      child: Text("DetailView"),
      onPressed: () => Navigator.of(context).push(MaterialPageRoute(
          builder: (context) => ChangeNotifierProvider(
              create: (_) => ViewModel(), child: DetailScreen()))),
    )));
  }
}

class DetailScreen extends StatelessWidget {
  const DetailScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: RaisedButton(
        child: Text("EditScreen"),
        onPressed: () => Navigator.of(context)
            .push(MaterialPageRoute(builder: (context) => EditScreen())),
      ),
    ));
  }
}

class EditScreen extends StatelessWidget {
  const EditScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: RaisedButton(
            child: Text("Print"),
            onPressed: () =>
                Provider.of<ViewModel>(context, listen: false).printNumber()),
      ),
    );
  }
}

class ViewModel extends ChangeNotifier {
  printNumber() {
    print(2);
  }
}

【问题讨论】:

    标签: flutter dart


    【解决方案1】:

    为了能够跨导航访问提供程序,您需要在 MaterialApp 之前提供它,如下所示

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return ChangeNotifierProvider(
          create: (_) => ViewModel(),
          child: MaterialApp(
            title: 'Flutter Demo',
            theme: ThemeData(
              primarySwatch: Colors.blue,
            ),
            home: MyHomePage(),
          ),
        );
      }
    }
    
    class MyHomePage extends StatelessWidget {
      const MyHomePage({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            body: Center(
                child: RaisedButton(
          child: Text("DetailView"),
          onPressed: () => Navigator.of(context).push(
            MaterialPageRoute(
              builder: (context) => DetailScreen(),
            ),
          ),
        )));
      }
    }
    
    class DetailScreen extends StatelessWidget {
      const DetailScreen({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            body: Center(
          child: RaisedButton(
            child: Text("EditScreen"),
            onPressed: () => Navigator.of(context)
                .push(MaterialPageRoute(builder: (context) => EditScreen())),
          ),
        ));
      }
    }
    
    class EditScreen extends StatelessWidget {
      const EditScreen({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: RaisedButton(
                child: Text("Print"),
                onPressed: () =>
                    Provider.of<ViewModel>(context, listen: false).printNumber()),
          ),
        );
      }
    }
    
    class ViewModel extends ChangeNotifier {
      printNumber() {
        print(2);
      }
    }
    

    【讨论】:

    • 感谢您的回答。我知道这行得通,但我不能这样做,因为我不仅有一个 ViewModel,而且每个 DetailScreen 都有一个 ViewModel。
    • 非常感谢您的帮助。我知道本教程,但在我的情况下它没有帮助,如果我想按照我在问题中提到的方式来做同样的问题。雷米的回答刚刚提到我需要在我的路线中包含提供者,但不是它是如何完成的。这实际上是我的问题:/ 我知道我可以稍微不同地设计我的应用程序,但其他方式对我来说并不自然,因此我的问题
    • 你找到这个问题的答案了吗@JosefWilhelm?
    • 我对@JosefWilhelm 有同样的问题。将 provider/multi provider 放在 MaterialApp 之前不是我们想要的,即使它会起作用。
    • 尝试来自同一开发者的新riverpod状态管理,完全重写提供者
    【解决方案2】:

    我有点晚了,但我找到了一个解决方案,说明如何在Navigator.push() 之后保持Provider 的值,而不必将Provider 放在MaterialApp 上方。

    为此,我使用了库custom_navigator。它允许您在树中的任意位置创建Navigator

    您将必须创建 2 个不同的 GlobalKey&lt;NavigatorState&gt;,并将它们提供给 MaterialAppCustomNavigator 小部件。这些键将允许您控制要使用的导航器。

    这里有一个小sn-p来说明怎么做

    class App extends StatelessWidget {
    
       GlobalKey<NavigatorState> _mainNavigatorKey = GlobalKey<NavigatorState>(); // You need to create this key for the MaterialApp too
    
       @override
       Widget build(BuildContext context) {
          return MaterialApp(
             navigatorKey: _mainNavigatorKey;  // Give the main key to the MaterialApp
             home: Provider<bool>.value(
                value: myProviderFunction(),
                child: Home(),
             ),
          );
       }
    
    }
    
    class Home extends StatelessWidget {
    
       GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>(); // You need to create this key to control what navigator you want to use
    
       @override
       Widget build(BuildContext context) {
    
          final bool myBool = Provider.of<bool>(context);
    
          return CustomNavigator (
             // CustomNavigator is from the library 'custom_navigator'
             navigatorKey: _navigatorKey,  // Give the second key to your CustomNavigator
             pageRoute: PageRoutes.materialPageRoute,
             home: Scaffold(
                body: FlatButton(
                   child: Text('Push'),
                   onPressed: () {
                      _navigatorKey.currentState.push(  // <- Where the magic happens
                         MaterialPageRoute(
                            builder: (context) => SecondHome(),
                         ),
                      },
                   ),
                ),
             ),
          );   
       }
    }
    
    class SecondHome extends StatelessWidget {
    
       @override
       Widget build(BuildContext context) {
    
          final bool myBool = Provider.of<bool>(context);
    
          return Scaffold(
             body: FlatButton(
                child: Text('Pop'),
                onPressed: () {
                   Novigator.pop(context);
                },
             ),
          );
       }
    
    }
    

    在这里,您可以从Home 小部件中的Provider 读取值myBool,即使在Navigator.push() 之后也可以在SecondHome 小部件中读取值。

    但是,Android 的back 按钮会从MaterialApp 的导航器中触发Navigator.pop()。如果你想使用CustomNavigator的那个,你可以这样做:

    // In the Home Widget insert this
       ...
       @override
       Widget build(BuildContext context) {
          return WillPopScope(
             onWillPop: () async {
                if (_navigatorKey.currentState.canPop()) {
                   _navigatorKey.currentState.pop();  // Use the custom navigator when available
                   return false;  // Don't pop the main navigator
                } else {
                   return true;  // There is nothing to pop in the custom navigator anymore, use the main one
                }
             },
             child: CustomNavigator(...),
          );
       }
       ...
    

    【讨论】:

      【解决方案3】:

      聚会有点晚了,但我认为这是问题所要寻找的答案:

      (基本上将 ViewModel 传递到下一个 Navigator 页面。)

      class DetailScreen extends StatelessWidget {
        const DetailScreen({Key key}) : super(key: key);
      
        @override
        Widget build(BuildContext context) {
          final viewModel = Provider.of<ViewModel>(context); // Get current ViewModel
          return Scaffold(
              body: Center(
            child: RaisedButton(
              child: Text("EditScreen"),
              onPressed: () => Navigator.of(context).push(
                // Pass ViewModel down to EditScreen
                MaterialPageRoute(builder: (context) {
                  return ChangeNotifierProvider.value(value: viewModel, child: EditScreen());
                }),
              ),
            ),
          ));
        }
      }
      
      

      【讨论】:

        【解决方案4】:

        推送时使用Navigator拆分路线

        首先,您需要在DetailScreen() 中添加Navigator。之后,navigator 可以在嵌套的 navigator 上推送新的路由基础。

        class DetailScreen extends StatelessWidget {
        
          ...
        
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              body: Navigator(
                onGenerateRoute: (settings) {
                  return MaterialPageRoute(
                    builder: (nestedContext) {
        
                      ...
                      /// Here you can push navigator with nested context
                      ...
        
                    },
                  );
                },
              ),
            );
          }
        

        使用 WillPopScope 捕获返回事件

        其次,你会发现Android平台的“返回”按钮会同时弹出“DetailScreen + EditScreen”,可能和你想象的不一样。

        如果你想处理这个,你需要捕获后退按钮并使用 WillPopScope() 手动处理。有两种方法可以获取嵌套的导航器状态。

        1. 将 GlobalKey 分配给 Navigator 并从 Globalkey 中获取 NavigatorState
        2. 创建一个局部变量来跟踪NavigatorState

        我以第二个为例:

        class DetailScreen extends StatelessWidget {
        
        ...
        
        @override
        Widget build(BuildContext context) {
        
          NavigatorState navigatorState;
        
          return WillPopScope(
            onWillPop: () async {
              if(navigatorState.canPop()){
                navigatorState.pop();
                return false;
              }else{
                return true;
              }
            },
            child: Scaffold(
        
              ...
        
              return MaterialPageRoute(
                builder: (nestedContext) {
                  navigatorState = Navigator.of(nestedContext);
                  ...
        
                }
              )
        

        毕竟,你可以得到如下所示的树

        这是DetailScreen的完整代码

        class DetailScreen extends StatelessWidget {
        
          const DetailScreen({Key key}) : super(key: key);
        
          @override
          Widget build(BuildContext context) {
        
            NavigatorState navigatorState;
            return WillPopScope(
              onWillPop: () async {
                if (navigatorState.canPop()) {
                  navigatorState.pop();
                  return false;
                } else {
                  return true;
                }
              },
              child: Scaffold(
                body: Navigator(
                  onGenerateRoute: (settings) {
                    return MaterialPageRoute(
                      builder: (nestedContext) {
                        navigatorState = Navigator.of(nestedContext);
                        return Center(
                          child: RaisedButton(
                            child: Text("EditScreen"),
                            onPressed: () => Navigator.of(nestedContext).push(
                              MaterialPageRoute(builder: (_) => EditScreen()),
                            ),
                          ),
                        );
                      },
                    );
                  },
                ),
              ),
            );
          }
        }
        

        英雄动画不再起作用

        这是我最近遇到的问题。这是因为 Navigator 中默认的observers 是一个空数组。

        RectTween _createRectTween(Rect begin, Rect end) {
          return MaterialRectArcTween(begin: begin, end: end);
        }
        
        ...
        Navigator(
          observers: [HeroController(createRectTween: _createRectTween)],
        

        您可以详细了解如何添加此视频中提到的您赢得的自定义导航器: https://www.youtube.com/watch?v=EcAwFpC9S8s&ab_channel=Flutter

        【讨论】:

        • 这绝对与问题无关
        • GGirotto 你能解释为什么这个答案与问题无关吗?我真的很想知道我是如何误解这一点的。谢谢~
        • 我认为就今天而言,riverpod.dev 现在已经做出了很好的解决方案,而无需在 MaterialApp 周围放置 ChangeNotifier。
        猜你喜欢
        • 2021-02-07
        • 2020-12-10
        • 2020-10-29
        • 1970-01-01
        • 2020-04-13
        • 2021-02-15
        • 2019-09-17
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多