英文:
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<ViewModel> 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 '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);
}
}
答案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 '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);
}
}
答案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<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());
}),
),
),
));
}
}
答案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<NavigatorState>
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<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);
},
),
);
}
}
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't pop the main navigator
} else {
return true; // There is nothing to pop in the custom navigator anymore, use the main one
}
},
child: CustomNavigator(...),
);
}
...
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论