Flutter Provider嵌套导航

huangapple go评论75阅读模式
英文:

Flutter Provider nested navigation

问题

我在提供程序和导航方面遇到了问题。

我有一个名为HomeScreen的屏幕,其中包含一个对象列表。当单击一个对象时,我导航到一个带有选项卡导航的DetailScreen。此DetailScreen包装在一个ChangenotifierProvider中,该提供程序提供了一个ViewModel

现在,当我导航到另一个屏幕并使用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 (包装在ChangeNotifierProvider中)
   - Edit Screen
     - 从这里访问提供程序

我知道问题出在哪里。我在堆栈上推送了一个新屏幕,而更改通知器不再可用。我考虑创建一个Detail Repository,它位于我的应用程序之上,保存了DetailView的所有ViewModel。

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

我真的不知道如何解决这个问题。感谢大家的帮助。

这是一个快速示例应用程序:

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

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

class MyApp extends StatelessWidget {
  @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);
  }
}
英文:

I have a problem with provider and navigation.

I have a HomeScreen with a list of objects. When you click on one object I navigate to a DetailScreen with tab navigation. This DetailScreen is wrapped with a ChangenotifierProvider which provides a ViewModel

Now, when I navigate to another screen with Navigator.of(context).push(EditScreen) I can't access the ViewModel within the EditScreen
The following error is thrown

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

This is a simple overview of what I try to achieve

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

I know what the problem is. I'm pushing a new screen on the stack and the change notifier is not available anymore.
I thought about creating a Detail Repository on top of my App which holds all of the ViewModels for the DetailView.

I know I could wrap the ChangeNotifier around my MaterialApp, but I don't want that, or can't do it because I don't know which Detail-ViewModel I need. I want a ViewModel for every item in the list

I really don't know what's the best way to solve this. Thanks everyone for the help

Here is a quick example app:

This is a picture of the image tree

import &#39;package:flutter/material.dart&#39;;
import &#39;package:provider/provider.dart&#39;;

void main() =&gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &#39;Flutter Demo&#39;,
      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(&quot;DetailView&quot;),
      onPressed: () =&gt; Navigator.of(context).push(MaterialPageRoute(
          builder: (context) =&gt; ChangeNotifierProvider(
              create: (_) =&gt; 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(&quot;EditScreen&quot;),
        onPressed: () =&gt; Navigator.of(context)
            .push(MaterialPageRoute(builder: (context) =&gt; 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(&quot;Print&quot;),
            onPressed: () =&gt;
                Provider.of&lt;ViewModel&gt;(context, listen: false).printNumber()),
      ),
    );
  }
}

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

答案1

得分: 24

为了能够在不同页面之间访问提供者(Provider),您需要在 MaterialApp 之前提供它,如下所示:

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

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

class MyApp extends StatelessWidget {
  @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);
  }
}
英文:

To be able to access providers accross navigations, you need to provide it before MaterialApp as follows

import &#39;package:flutter/material.dart&#39;;
import &#39;package:provider/provider.dart&#39;;

void main() =&gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) =&gt; ViewModel(),
      child: MaterialApp(
        title: &#39;Flutter Demo&#39;,
        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(&quot;DetailView&quot;),
      onPressed: () =&gt; Navigator.of(context).push(
        MaterialPageRoute(
          builder: (context) =&gt; 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(&quot;EditScreen&quot;),
        onPressed: () =&gt; Navigator.of(context)
            .push(MaterialPageRoute(builder: (context) =&gt; 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(&quot;Print&quot;),
            onPressed: () =&gt;
                Provider.of&lt;ViewModel&gt;(context, listen: false).printNumber()),
      ),
    );
  }
}

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

答案2

得分: 18

迟来的一点,但我认为这是问题所寻找的答案:

(基本上是将 ViewModel 传递给下一个导航器页面。)

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

  @override
  Widget build(BuildContext context) {
    final viewModel = Provider.of<ViewModel>(context); // 获取当前 ViewModel
    return Scaffold(
        body: Center(
      child: RaisedButton(
        child: Text("EditScreen"),
        onPressed: () => Navigator.of(context).push(
          // 将 ViewModel 传递给 EditScreen
          MaterialPageRoute(builder: (context) {
            return ChangeNotifierProvider.value(value: viewModel, child: EditScreen());
          }),
        ),
      ),
    ));
  }
}

注意:上述代码段中包含的部分内容并没有进行翻译,因为它们是代码,不需要翻译。

英文:

A bit late to the party, but I think this is the answer the question was looking for:

(Basically passing the ViewModel down to the next Navigator page.)

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

  @override
  Widget build(BuildContext context) {
    final viewModel = Provider.of&lt;ViewModel&gt;(context); // Get current ViewModel
    return Scaffold(
        body: Center(
      child: RaisedButton(
        child: Text(&quot;EditScreen&quot;),
        onPressed: () =&gt; Navigator.of(context).push(
          // Pass ViewModel down to EditScreen
          MaterialPageRoute(builder: (context) {
            return ChangeNotifierProvider.value(value: viewModel, child: EditScreen());
          }),
        ),
      ),
    ));
  }
}

答案3

得分: 3

这里是代码的翻译部分:

我有点晚,但我找到了一个解决方案,可以在`Navigator.push()`后保持`Provider`的值保持不变,而无需将`Provider`放在`MaterialApp`之上。

为此,我使用了库[`custom_navigator`](https://pub.dev/packages/custom_navigator)。它允许您在树的任何位置创建一个`Navigator`。

您将需要创建两个不同的`GlobalKey<NavigatorState>`,并将它们分别分配给`MaterialApp`和`CustomNavigator`小部件。这些密钥将允许您控制要使用的Navigator

以下是一个小片段,用于说明如何执行:

```dart
class App extends StatelessWidget {

   GlobalKey<NavigatorState> _mainNavigatorKey = GlobalKey<NavigatorState>(); // 您需要为MaterialApp也创建此密钥

   @override
   Widget build(BuildContext context) {
      return MaterialApp(
         navigatorKey: _mainNavigatorKey;  // 将主要密钥分配给MaterialApp
         home: Provider<bool>.value(
            value: myProviderFunction(),
            child: Home(),
         ),
      );
   }

}

class Home extends StatelessWidget {

   GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>(); // 您需要创建此密钥以控制要使用的导航器

   @override
   Widget build(BuildContext context) {

      final bool myBool = Provider.of<bool>(context);

      return CustomNavigator (
         // CustomNavigator来自库'custom_navigator'
         navigatorKey: _navigatorKey,  // 将第二个密钥分配给CustomNavigator
         pageRoute: PageRoutes.materialPageRoute,
         home: Scaffold(
            body: FlatButton(
               child: Text('Push'),
               onPressed: () {
                  _navigatorKey.currentState.push(  // <- 这是魔法发生的地方
                     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: () {
               Navigator.pop(context);
            },
         ),
      );
   }

}

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

然而,Android的返回按钮将触发从MaterialApp的Navigator中的Navigator.pop()。如果您想使用CustomNavigator的一个,您可以这样做:

// 在Home小部件中插入此代码
   ...
   @override
   Widget build(BuildContext context) {
      return WillPopScope(
         onWillPop: () async {
            if (_navigatorKey.currentState.canPop()) {
               _navigatorKey.currentState.pop();  // 当可用时使用自定义导航器
               return false;  // 不要弹出主导航器
            } else {
               return true;  // 在自定义导航器中没有要弹出的内容,使用主导航器
            }
         },
         child: CustomNavigator(...),
      );
   }
   ...
英文:

I am a bit late but I found a solution on how to keep the value of a Provider alive after a Navigator.push() without having to put the Provider above the MaterialApp.

To do so, I have used the library custom_navigator. It allows you to create a Navigator wherever you want in the tree.

You will have to create 2 different GlobalKey&lt;NavigatorState&gt; that you will give to the MaterialApp and CustomNavigator widgets. These keys will allow you to control what Navigator you want to use.

Here is a small snippet to illustrate how to do

class App extends StatelessWidget {

   GlobalKey&lt;NavigatorState&gt; _mainNavigatorKey = GlobalKey&lt;NavigatorState&gt;(); // 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&lt;bool&gt;.value(
            value: myProviderFunction(),
            child: Home(),
         ),
      );
   }

}

class Home extends StatelessWidget {

   GlobalKey&lt;NavigatorState&gt; _navigatorKey = GlobalKey&lt;NavigatorState&gt;(); // You need to create this key to control what navigator you want to use

   @override
   Widget build(BuildContext context) {

      final bool myBool = Provider.of&lt;bool&gt;(context);

      return CustomNavigator (
         // CustomNavigator is from the library &#39;custom_navigator&#39;
         navigatorKey: _navigatorKey,  // Give the second key to your CustomNavigator
         pageRoute: PageRoutes.materialPageRoute,
         home: Scaffold(
            body: FlatButton(
               child: Text(&#39;Push&#39;),
               onPressed: () {
                  _navigatorKey.currentState.push(  // &lt;- Where the magic happens
                     MaterialPageRoute(
                        builder: (context) =&gt; SecondHome(),
                     ),
                  },
               ),
            ),
         ),
      );   
   }
}

class SecondHome extends StatelessWidget {

   @override
   Widget build(BuildContext context) {

      final bool myBool = Provider.of&lt;bool&gt;(context);

      return Scaffold(
         body: FlatButton(
            child: Text(&#39;Pop&#39;),
            onPressed: () {
               Novigator.pop(context);
            },
         ),
      );
   }

}

Here you can read the value myBool from the Provider in the Home widget but also ine the SecondHome widget even after a Navigator.push().

However, the Android back button will trigger a Navigator.pop() from the Navigator of the MaterialApp. If you want to use the CustomNavigator's one, you can do this:

// 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&#39;t pop the main navigator
            } else {
               return true;  // There is nothing to pop in the custom navigator anymore, use the main one
            }
         },
         child: CustomNavigator(...),
      );
   }
   ...

huangapple
  • 本文由 发表于 2020年1月3日 18:00:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/59576495.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定