I have a problem in using 'StreamProvider' and having 'Provider.of' delivering the goodies. I have two interlinked issues. (1) is I can't get ProxyProvider to work (and most likely I am just misunderstanding it, and it may not be the solution). (2) The widget Drawer() (or the way I am using it) seems to block 'Provider.of'. 'StreamProvider' can't be found up the tree.
In MyApp() I initially had:
return MultiProvider(
providers: [
StreamProvider<User>.value(value: AuthService().user),
StreamProvider<UserData>.value(
value: DatabaseServices(uid: 'user.userUid').userData),
StreamProvider<QuerySnapshot>.value(
value: DatabaseServices(uid: 'user.userUid').allTasks),
StreamProvider<List<TaskDetails>>.value(
value: DatabaseServices(uid: 'user.userUid').userTasksByImportance),
StreamProvider<List<PropertyDetails>>.value(
value: DatabaseServices(uid: 'UuO2DO0JUVbVD0R1JqIclI7fprF3')
.userProperties),
],
child: MaterialApp(
theme: themeData,
home: Wrapper(),
routes: {
SettingsForm.id: (context) => SettingsForm(),
TaskList.id: (context) => TaskList(),
AddTask.id: (context) => AddTask(),
EditTask.id: (context) => EditTask(),
PropertyList.id: (context) => PropertyList(),
AddProperty.id: (context) => AddProperty(),
AddUnit.id: (context) => AddUnit(),
TestMarkdown.id: (context) => TestMarkdown(),
},
),
);
I tried to use ProxyProvider to supply a user.userUid from StreamProvider to the other StreamProviders below it but couldn't get anything to work.
Next in Wrapper() I tried this:
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
if (user == null) {
return Authenticate();
} else {
return MultiProvider(
providers: [
StreamProvider<UserData>.value(
value: DatabaseServices(uid: user.userUid).userData),
StreamProvider<List<TaskDetails>>.value(
value: DatabaseServices(uid: user.userUid).userTasksByImportance),
StreamProvider<QuerySnapshot>.value(
value: DatabaseServices(uid: user.userUid).allTasks),
StreamProvider<List<PropertyDetails>>.value(
value: DatabaseServices(uid: user.userUid).userProperties),
],
child: Home(),
);
}
}
}
Now this is below MaterialApp() which I didn't think was a good idea, but I could make user.user.Uid available to the other StreamProvider(s) and all worked. Provider.of in Home() and HomePage() (called by Home()) supplied the goodies.
Now Home() has a Drawer:
return Scaffold(
drawer: buildDrawer(context),
backgroundColor: Colors.blueAccent[50],
appBar: appBar,
body: Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
decoration: BoxDecoration(
color: Colors.white,
image: DecorationImage(
alignment: Alignment.bottomCenter,
image: AssetImage('assets/property_returns_logo_drawn.png'),
fit: BoxFit.fitWidth),
),
child: HomePage(),
),
);
}
}
Drawer buildDrawer(BuildContext context) {
final user = Provider.of<User>(context);
final userData = Provider.of<UserData>(context);
final userProperties = Provider.of<List<PropertyDetails>>(context);
final double _minHeight = 40;
final double _maxHeight = 40;
return Drawer(
child: ListView(
padding: EdgeInsets.all(0),
children: <Widget>[
DrawerHeader(
and userProperties is available within the drawer itself, but any widget called from the drawer gives the error that it can't find a relevant StreamProvider for the Provider.of. If I reput the relevant StreamProvider back in MyAPP() the relevant StreamProvider can be found. But with no values from Firestore unless I fudge and for user.userUid hardcode a valid uid:. Obviously not a solution but works as far as the widgets called by Drawer is concerned.
What I am looking for is to understand how to use ProxyProvider, or if that is not the solution, how to structure my code. Maybe I can use StreamProviders (or StreamBuilders) in every widget called from Drawer but that doesn't feel right. Code is at https://github.com/sctajc/property_returns.
As of today, ProxyProvider doesn't work as the StreamProvider, so you cannot use it the way you want.
To answer why your Drawer has access to Provider.of<Foo> while the routes opened from it haven't, is because the routes are defined in your MaterialApp.
What does this mean? It means that while Drawer is below Wrapper and thus, has access to Provider.of<Foo>, the other routes are technically above the Wrapper widget.
TL:DR
The solution would be just creating one ProxyProvider of your DatabaseServices like this:
ProxyProvider<User, DatabaseServices>(
update: (_, user, __) => DatabaseServices(uid: user?.userUid),
),
And then, if you need to tap into the UserData, you just need to do:
Provider.of<DatabaseServices>(context).userData
This way you avoid creating 4 instances of the same object (DatabaseServices) and at the same time, you solve your problem. However, the logic of your app will need to change a bit to make it work, I am sure it won't be a problem.
I hope this can help you and you can keep on developing your app!
Related
Can i showDialog inside a function without passing context?
void test(){
showDialog(context: context, builder: (_) => AlertDialog(
content: Column(
children: [
Row(
children: const [
Icon(Icons.check_circle, color: Colors.green,),
Text("Hi"),
],
)
],
),
));
}
Sorry i didn't explain very well, without passing context to function, not to showDialog
According to the doc (https://api.flutter.dev/flutter/material/showDialog.html) you can't, it's required.
the short answer is no, you can't.
the long answer:
first, the BuildContext is a object type, so in order to remove conflictions between the context property and the context value we're going to rename it to contextGotFromUI.
Note: contextGotFromUI here is just a BuildContext object sp we can rename it with whatever we want.
just to not get confused by the same names
void test(){
showDialog(context: contextGotFromUI, builder: (_) => AlertDialog(
content: Column(
children: [
Row(
children: const [
Icon(Icons.check_circle, color: Colors.green,),
Text("Hi"),
],
)
],
),
));}
the context property in the showDialog is required to set from it's implementation:
Future<T?> showDialog<T>({
required BuildContext context,
required WidgetBuilder builder,
bool barrierDismissible = true,
// more code
the BuildContext is an important topic to understand in flutter, to show a dialog widget on top of the screen the user is actually navigating in and seeing at any time, the BuildContext is what tells to show it on top of the widget with that specific context, and not other screens.
As from the showDialog official documentation:
The context argument is used to look up the Navigator and Theme for the dialog. It is only used when the method is called. Its corresponding widget can be safely removed from the tree before the dialog is closed.
so in order to show a dialog from an external method, you need to pass a context that belongs to a specific widget, then use it in the showDialog:
void test(BuildContext contextGotFromUI){
showDialog(context: contextGotFromUI, builder: (_) => AlertDialog(
content: Column(
children: [
Row(
children: const [
Icon(Icons.check_circle, color: Colors.green,),
Text("Hi"),
],
)
],
),
));}
then from your UI where you're calling that method, pass it:
Widget build(BuildContext) {
// widgets
//...
onPressed: () {
test(context); // will show an expected dialog on the screen
}
}
Yes, you can but you have to create the function inside a stateful widget not in the normal classes.
in case you create the function in a normal class the context will be required!
void test(BuildContext context){
showDialog(context: context, builder: (_) => AlertDialog(
content: Column(
children: [
Row(
children: const [
Icon(Icons.check_circle, color: Colors.green,),
Text("Hi"),
],
)
],
),
));
}
I have an issue with the navigation process in my Flutter app.
First, I have a home page that has a button. When I press search, I go to the search page, obviously, and the results will appear on the home page again.
If no results were found, a dialog would appear that says "Try again" and has an OK button.
I want to press the OK button to navigate to the search page again. But there is a problem with the scaffolds like the image below. How can I fix this?
And my home page code, for alert dialog is like this:
Widget build(BuildContext context) {
print(widget.selectedURL);
return MaterialApp(
home: Center(
child: FutureBuilder<List<Pet>>(
future: API.get_pets(widget.selectedURL),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Pet>? pet_data = snapshot.data;
print(pet_data?.length);
return Padding(
padding: const EdgeInsets.all(8.0),
child:(pet_data?.length == 0)
? AlertDialog(
title: const Text("Failed"),
alignment: Alignment.topCenter,
content: const Text(
'No Pets Found. Please try different search options.'),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () => Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const Search())),
)
],
)
: ListView.builder(
//showing the list of results
)
The problem is you are wrapping everything inside a MaterialApp.
MaterialApp defines a Navigator property, which means it should be at the top-level of your application and it should be unique. If you have two different MaterialApp they will use their own Navigator, hence they will not share routes. See more informations here MaterialApp.
The usual design is:
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: YourWidgets(
[...]
)//YourWidgets
)//Scaffold
);//MaterialApp
}
Try removing MaterialApp, or changing your MaterialApp to something else, like Material if you really need additional customization.
I'm new to flutter, would like to do a List to display the image and title in Grid View Builder however I met an error. I also have to call out the list however not sure what's the issue for the error which I'm don't know how to fix. Error coming out on the code with "appItem.items[i].appImage". Much appreciate your help!
List<AppProfile> get items {
return [..._items];
}
My array(still have quite a lot however only take these two for example)
List<AppProfile> _items = [
AppProfile(
appID: "",
title: "Whatsapp",
appImage: "assets/images/Materials-03.png",
appLink: "",
),
AppProfile(
appID: "",
title: "Wechat",
appImage: "assets/images/Materials-04.png",
appLink: "",
),
]
in my builder/ display file
Widget build(BuildContext context) {
final appItem = Provider.of<AppLogic>(context, listen: false);
return Scaffold(
appBar: AppBar(
title: const Text("Your Social Media"),
),
body:
Container(
child: GridView.builder(
padding: EdgeInsets.all(30),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2 / 2,
crossAxisSpacing: 30, //spacing between column
mainAxisSpacing: 20,
),
itemCount: appItem.items.length,
itemBuilder: (ctx, i) {
return GestureDetector(
onTap: () {},
child: Column(
children: <Widget> [
**appItem.items[i].appImage**,
]
),
);
},
),
),
);
Error occurs
Error: Could not find the correct Provider above this SocialProfileScreen Widget
This happens because you used a BuildContext that does not include the provider
of your choice. There are a few common scenarios:
You added a new provider in your main.dart and performed a hot-reload.
To fix, perform a hot-restart.
The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
You used a BuildContext that is an ancestor of the provider you are trying to read.
Make sure that SocialProfileScreen is under your MultiProvider/Provider.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because `context` is associated
// to the widget that is the parent of `Provider<Example>`
child: Text(context.watch<Example>()),
),
}
However in my home page have floating button
Widget build(BuildContext context) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
// "Navigator.push" returns a Future and "onPressed" accepts void
//so cannot directly use "Navigator" but if using () =>
//simply means that executing "Navigator..."
onPressed: () =>
Navigator.of(context).pushNamed(SocialProfileScreen.routeName),
),
The error occurs because your appItem.items[i].appImage returns a String instead of a Widget. In this case, you need to wrap it with a Widget that displays an image.
However, you can't use a String, double, bool or other variables into a list which is the Widgets list.
So you should add an Image.assets() widget or another one (which displays images), and give your image path.
Like:
Image.asset(appItem.items[i].appImage);
I want to create a transition immediately from one screen to another (no animation). However isInitialRoute generates an error due to an update to the Router widget. The suggestion is to use onGenerateInitialRoutes but I am not sure how to implement it.
The flutter design doc recommends this:
Routes & RouteSettings
Currently, the only way to add a Route to the history stack without playing its entrance animation is to mark it as an initial route in its RouteSettings. The declarative API requires that routes can be added without animation at any time. This is useful when the route is covered by another route and playing the animation simply doesn't make sense. To support this, a didAdd method is added to the Route interface. This method is called by the Navigator instead of didPush when the Route should just be added without its regular entrance animation. To simplify things, this new method will also be used to bring the initial route on screen. This makes the RouteSettings.initialRoute parameter useless and it will be removed from RouteSettings. This is a minor breaking change.
This is the code that is generating the error on isInitialRoute:
import 'package:flutter/cupertino.dart'
show
CupertinoApp,
CupertinoButton,
CupertinoPageRoute,
CupertinoPageScaffold;
import 'package:flutter/widgets.dart'
show
BuildContext,
Center,
Column,
Navigator,
Route,
RouteSettings,
SafeArea,
Spacer,
Text,
runApp,
Widget;
Widget makeButton(BuildContext context, String routeName) =>
new CupertinoButton(
onPressed: () => Navigator.pushReplacementNamed(context, routeName),
child: Text('Go to \'$routeName\''),
);
Route generateRoute(RouteSettings settings) {
switch (settings.name) {
case 'not-animated':
return new CupertinoPageRoute(
settings: RouteSettings(name: settings.name, isInitialRoute: true),
builder: (context) => CupertinoPageScaffold(
child: SafeArea(
child: Center(
child: Column(
children: [
Spacer(),
Text('This is \'not-animated\''),
makeButton(context, 'animated'),
Spacer(),
],
),
),
),
),
);
default:
return null;
}
}
void main() {
runApp(
CupertinoApp(
onGenerateRoute: generateRoute,
initialRoute: 'animated',
routes: {
'animated': (context) => CupertinoPageScaffold(
child: SafeArea(
child: Center(
child: Column(
children: [
Spacer(),
Text('This is \'animated\''),
makeButton(context, 'not-animated'),
Spacer(),
],
),
),
),
),
},
),
);
}
If You Want to just navigate your screen add this to your code
onTap: (){
Navigator.push(
context,
MaterialPageRoute(builder: (context) => yoursecondscreenname()),
);
},
sending data between screen using Provider with Navigator concept make a conflict
error after the run
The following ProviderNotFoundError was thrown building SecondRoute(dirty):
Error: Could not find the correct Provider above this SecondRoute Widget
To fix, please:
Ensure the Provider is an ancestor to this SecondRoute Widget
Provide types to Provider
Provide types to Consumer
Provide types to Provider.of()
Always use package imports. Ex: `import 'package:my_app/my_code.dart';
Ensure the correct context is being used.
https://www.ideone.com/xHXK5m
you can pass data like this put data in class
RaisedButton(
child: Text(‘Send data to the second page’),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(
data: data,
)),
);
},
),
and recieve data like this
class SecondPage extends StatelessWidget {
final Data data;
SecondPage({this.data});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(‘Constructor — second page’),
),
body: Container(
padding: EdgeInsets.all(12.0),
alignment: Alignment.center,
child: Column(
children: <Widget>[
Container(
height: 54.0,
padding: EdgeInsets.all(12.0),
child: Text(‘Data passed to this page:’,
style: TextStyle(fontWeight: FontWeight.w700))),
Text(‘Text: ${data.text}’),
Text(‘Counter: ${data.counter}’),
Text(‘Date: ${data.dateTime}’),
],
),
),
);
}
You can try placing whatever Provider value you have above the MaterialApp and thus the navigation or, when you push to a second page, provide the value again. Providers are scoped to widget trees, so this is expected behavior.
As an example, to pass a value to another route, you could do something like
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
final foo = 'foo';
Provider<String>.value(
value: foo,
child: NewPage(),
);
}));
},
And then just consume it like regular in the NewPage route.
Try this code:
MaterialApp(
title: 'Navigation Basics',
home: ChangeNotifierProvider(
builder: (context) => Data(),
child: FirstRoute(),
))
Delete ChangeNotifierProvider in FirstRoute