I'm using Provider in my flutter app, and when I go to a new page, the data provided to the Provider at page 1 is not accessible in page 2.
The way I understood the way Provider works, was that there is a central place where one stores all the data, and one can access that data anywhere in the application. So in my application, which is shown below, ToDoListManager is the place where all the data is stored. And if I set the data in Page 1, then I will be able to access that data in Page 2, and vice versa.
If this is not correct, then what part is wrong? And why isn't it working in my application?
Here's the code
Page 1
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
builder: (context) => ToDoListManager(),
child: Scaffold(
appBar: AppBar(
title: Text('Cool Project'),
),
body:e ToDoList(),
),
);
}
}
class ToDoList extends StatelessWidget {
#override
Widget build(BuildContext context) {
final toDoListManager = Provider.of<ToDoListManager>(context);
return ListView.builder(
itemCount: toDoListManager.toDoList.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => Details(index)));
},
child: Text(toDoListManager.toDoList[index]),
);
},
);
}
}
Page 2
class Details extends StatelessWidget {
final int index;
Details(this.index);
#override
build(BuildContext context) {
return ChangeNotifierProvider(
builder: (context) => ToDoListManager(),
child: Scaffold(
appBar: AppBar(
title: Text('Details Bro'),
),
body: AppBody(index)),
);
}
}
class AppBody extends StatelessWidget {
final int index;
AppBody(this.index);
#override
Widget build(BuildContext context) {
final toDoListManager = Provider.of<ToDoListManager>(context);
print(toDoListManager.toDoList);
return Text(toDoListManager.toDoList[1]);
}
}
ToDoListProvider
class ToDoListManager with ChangeNotifier {
List<String> _toDoList = ['yo', 'bro'];
List<String> get toDoList => _toDoList;
set toDoList(List<String> newToDoList) {
_toDoList = newToDoList;
notifyListeners();
}
}
You have 2 options:
Place your ChangeNotifierProvider above your MaterialApp so that is accesible from any of you Navigator routes.
Keep your Home widget as is but when pushing the new widget with the Navigator provide the original Manager.
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return Provider<ToDoListManager>.value(
value: toDoListManager,
child: Details(index),
);
},
),
);
},
With both approaches you don't need to create a new ChangeNotifierProvider in your details screen.
Related
I receive some errors when I try to push my flutter page from neighbourhoodList to individual neighbourhoods (e.g. neighbourhoodlist_admirality).
In my neighbourhoodlist, I would like to navigate to the individual neighbourhood pages when the user has clicked on the relevant neighbourhood. As I have not build the individual neighbourhood pages yet, I have linked them to an example page i.e. NeighbourhoodAdmirality.
This is my code for the neighbourhoodlist page:
import 'package:flutter/material.dart';
import 'indv_neighbourhoods/neighbourhoodlist_admirality.dart';
class NeighbourhoodList extends StatefulWidget {
NeighbourhoodList({ this.name = "name"});
final String name;
#override
_NeighbourhoodListState createState() => _NeighbourhoodListState();
}
class _NeighbourhoodListState extends State<NeighbourhoodList> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Neighbourhoods',
),
),
body: _buildListView(context),
);
}
ListView _buildListView(BuildContext context){
return ListView.builder(
itemCount: allNeighbourhoods.length,
itemBuilder: (BuildContext content, int index) {
NeighbourhoodList neighbourhoodlist = allNeighbourhoods[index];
return NeighbourhoodListTile(neighbourhoodlist);
});
}
}
class NeighbourhoodListTile extends ListTile {
NeighbourhoodListTile(NeighbourhoodList neighbourhoodlist)
: super(
title: Text(neighbourhoodlist.name),
trailing: Icon(Icons.arrow_forward),
onTap: (){
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NeighbourhoodAdmiralty(neigh1)),
);
}
);
}
List<NeighbourhoodList> allNeighbourhoods = [
NeighbourhoodList(name: 'Admiralty'),
NeighbourhoodList(name: 'Aljunied'),
NeighbourhoodList(name: 'Ang Mo Kio'),
];
This is my code for an example page that I want to direct my neighbourhoodlist to when each individual neighbourhood is directed.
import 'package:flutter/material.dart';
class NeighbourhoodAdmiralty extends StatefulWidget {
final String neigh1;
NeighbourhoodAdmiralty(this.neigh1);
#override
_NeighbourhoodAdmiraltyState createState() => _NeighbourhoodAdmiraltyState();
}
class _NeighbourhoodAdmiraltyState extends State<NeighbourhoodAdmiralty> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Admiralty"),
),
body: Center(child: Text('This is the individual neighbourhood page'),
),
);
}
}
As I am still a beginner, I am facing some errors and have a few questions on these:
Error 1 on neighbourhoodlist.dart: "Undefined name 'context'" under Navigator.push --> not sure why this happens as I have already passed the BuildContext in my methods above
Error 2 on neighbourhoodlist.dart: "Undefined name 'neigh1'" under Navigator.push --> I would like to redirect the neighbourhoodlist.dart page to the individual neighbourhood sheets but I'm not sure what I pass here, I have tried 'neigh1' (my variable in neighbourhoodlist_admirality), 'name' - the variable in NeighbourhoodList, and 'index' 0 the variable inNeighbourhoodListState but none of them seem to work so far.
Appreciate all your help in resolving this and thanks in advance !
When you do
class NeighbourhoodListTile extends ListTile {
NeighbourhoodListTile(NeighbourhoodList neighbourhoodlist)
: super(
title: Text(neighbourhoodlist.name),
trailing: Icon(Icons.arrow_forward),
onTap: (){
Navigator.push(context, MaterialPageRoute(builder: (context) => NeighbourhoodAdmiralty(neigh1)));
}
);
}
you can't access context in onTap because Flutter hasn't yet provided one to you in this point. If you need the context, use a StatelessWidget instead, where you can access it in the build method:
class NeighbourhoodListTile extends StatelessWidget {
final NeighbourhoodList neighbourhoodlist;
const NeighbourhoodListTile(this.neighbourhoodlist);
#override
Widget build(BuildContext context) {
// Here's your context ^^^^^^^
return ListTile(
title: Text(neighbourhoodlist.name),
trailing: Icon(Icons.arrow_forward),
onTap: (){
Navigator.push(context, MaterialPageRoute(
builder: (context) => NeighbourhoodAdmiralty(neigh1)));
}
);
}
}
As to your second error, there is also no neigh1 variable at this point. I don't know what your logic is, but I think you want to replace it with neighbourhoodlist.name:
class NeighbourhoodListTile extends StatelessWidget {
final NeighbourhoodList neighbourhoodlist;
const NeighbourhoodListTile(this.neighbourhoodlist);
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(neighbourhoodlist.name),
trailing: Icon(Icons.arrow_forward),
onTap: (){
Navigator.push(context, MaterialPageRoute(
builder: (context) => NeighbourhoodAdmiralty(neighbourhoodlist.name)));
}
);
}
}
I can't access a provider defined above a Scaffold from showModalBottomSheet in the FloatingActionButton.
I've defined a HomePage like so:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return Scaffold(
body: Consumer<MyProvider>(
builder: (context, provider, _) {
return Text(provider.mytext); // this works fine
}
),
floatingActionButton: MyFAB(), // here is the problem
);
}
)
}
}
And this is MyFAB:
class MyFAB extends StatefulWidget {
#override
_MyFABState createState() => _MyFABState();
}
class _MyFABState extends State<MyFAB> {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
...
onPressed: () => show(),
);
}
void show() {
showModalBottomSheet(
...
context: context,
builder: (BuildContext context) {
return Wrap(
children: [
...
FlatButton(
onPressed: Provider.of<MyProvider>(context, listen: false).doSomething(); //Can't do this
Navigator.pop(context);
)
],
);
}
);
}
}
Error: Could not find the correct Provider<MyProvider above this BottomSheet Widget.
Fixed by placing the provider above MaterialApp, as described here.
Bottom sheets are created at the root of the material app. If a prodiver is declared below the material app, a bottom sheet cannot access it because the provider is not an ancestor of the bottom sheet in the widget tree.
The screenshot below shows a widget tree: the whole app is inside Wrapper and the bottom sheet is not created inside Wrapper. It is created as another child of MaterialApp (with a root element Container in this case).
For your case:
// main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return MaterialApp(
home: HomePage(),
);
},
);
}
}
// home_page.dart
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: MyFAB()
);
}
}
This is caused by passing it the wrong context. Wrap your FAB to a Builder widget and pass it as builder property. This will take a new context and pass it to showModalBottomSheet. Also, you can do onPressed: show, it's more concise.
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return Scaffold(
body: Consumer<MyProvider>(
builder: (context, provider, _) {
return Text(provider.mytext); // this works fine
}
),
floatingActionButton: MyFAB(context), // here is the problem
);
}
)
}
}
class MyFAB extends StatefulWidget {
#override
_MyFABState createState() => _MyFABState();
}
class _MyFABState extends State<MyFAB> {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
...
onPressed: (context) => show(context),
);
}
void show(ctx) {
showModalBottomSheet(
...
context: ctx,
builder: (BuildContext context) {
return Wrap(
children: [
...
FlatButton(
onPressed: () {
Provider.of<MyProvider>(ctx, listen: false).doSomething(); //Can't do this
Navigator.pop(ctx)
};
)
],
);
}
);
}
}
SOLUTION
HomePage:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return Scaffold(
body: Consumer<MyProvider>(
builder: (context, provider, _) {
return Text(provider.mytext); // this works fine
}
),
floatingActionButton: MyFAB(context), // here is the problem
);
}
)
}
}
MyFAB:
class MyFAB extends StatefulWidget {
final BuildContext ctx;
MyFAB(this.ctx)
#override
_MyFABState createState() => _MyFABState();
}
class _MyFABState extends State<MyFAB> {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
...
onPressed: () => show(),
);
}
void show() {
showModalBottomSheet(
...
context: context,
builder: (BuildContext context) {
return Wrap(
children: [
...
FlatButton(
onPressed: Provider.of<MyProvider>(widget.ctx, listen: false).doSomething(); //Can't do this
Navigator.pop(context);
)
],
);
}
);
}
}
In my opinion: showModalBottomSheet builds a bottom sheet with context which comes from Material App
1st image
so when we return any Widget to show in the Bottom sheet it uses that Material app context as we can see in the builder property in the:1st image.
2ng Image: your code
so in your code, when you are writing: Provider.of(context, listen: false).doSomething(); it is using context from the builder: (BuildContext context) which is the context of Material App. we have to change this context in order to use this Provider without having to uplift the position of our Provider above the Material App.
Now if we want to keep using that context to get the benefits of that overlay and automatic detection of suitable themes and still want to use the context of a widget that does have access to our provider:
we can pass the context of the Widget which does have Provider access to the FAB, but we will have to keep passing that context through widgets till we need to use that Provider in our FAB or till we go to a different route: in which case we can start from a new context and provider as Providers are scoped in mature.
so in your HomePage either you can wrap your scaffold inside a Builder or you can create a new widget like this:"
3rd image
so that it will have its own context which does have access to the provider we need inside our FAB as shown below in 4th image:
4th image
and then in the builder property of showModalBottomSheet change the name of the parameter in an anonymous function so that it won't be confused with the MAterial App context and context we will be passing in (Builder context or IdeaScreen context in my case image 4th)
5th image
I am creating a new widget but you do not have need to do so you can directly write your Fab code inside the anonymous function:
and can use context(not newContext which is related to Material App context) while calling the Provider as you are already doing.
But I will show in my case What I am doing in my AddTask Widget in case anyone's use case is similar to mine:
6th image
expect a context, which does have a provider access, I my case its context of IdeaScreen.
and then use it just like this:
7th image
I'm using the BLOC pattern to authenticate a user in my app. I have a main BlocProvider that wraps my app. And a BlocBuilder to build according to the authentication state.
If the user is unauthenticated i have onboarding / intro screens that will navigate to the login screen.
The login screen is wrapped in another BlocProvider that contains a button that will do the login, and add a logged in event when the login is successful.
Problem is when i navigate from the onboarding screens i loose the main authenticationBloc context. What do i need to to to have access to the authentication bloc after i pushed a new screen.
void main() {
WidgetsFlutterBinding.ensureInitialized();
Bloc.observer = SimpleBlocObserver();
runApp(
MyApp(),
);
}
class AuthenticationWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider<AuthenticationBloc>(
create: (context) => AuthenticationBloc()..add(AppStarted()),
child: MyApp(),
),
);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocListener<AuthenticationBloc, AuthenticationState>(
listener: (context, state) {
if (state is Authenticated) {
_appUserProfileRepository = AppUserProfileRepository();
}
},
child: BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (context, state) {
_authCredentialHelper = state.authCredentialHelper;
if (state is Uninitialized) {
return SplashScreen();
}
if (state is Unauthenticated) {
return OnboardingScreens(authCredentialHelper: _authCredentialHelper);
}
if (state is InvalidRegistration) {
return RegisterProfileScreen(authCredentialHelper: _authCredentialHelper);
}
if (state is Authenticated) {
xxx
}
return Scaffold(body: Center(child: LoadingIndicator()));
},
),
);
}
}
This is the onboarding screen where i loose the authenticationbloc context as soon as i navigate
class OnboardingScreens extends StatelessWidget {
final AuthCredentialHelper authCredentialHelper;
OnboardingScreens({this.authCredentialHelper});
_pages(BuildContext context) {
return [
xxx
];
}
_getStartedClicked(BuildContext context) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return LoginScreen(authCredentialHelper: authCredentialHelper);
}));
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: IntroductionScreen(
pages: _pages(context),
onDone: () => _getStartedClicked(context),
showSkipButton: true,
done: xxx
),
),
);
}
}
When adding a breakpoint at 1. the context is fine with a valid value for BlocProvider.of(context)
Stepping to 2. gives me an error:
BlocProvider.of() called with a context that does not contain a Cubit of type AuthenticationBloc.
_getStartedClicked(BuildContext context) {
1----->Navigator.push(context, MaterialPageRoute(builder: (context) {
2----->return LoginScreen(authCredentialHelper: authCredentialHelper);
}));
}
This is the LoginScreen code
class LoginScreen extends StatelessWidget {
final AuthCredentialHelper authCredentialHelper;
LoginScreen({this.authCredentialHelper});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back, color: darkBlue),
onPressed: () => Navigator.of(context).pop(),
),
backgroundColor: Colors.transparent,
elevation: 0.0,
),
body: SafeArea(
child: Center(
child: BlocProvider<LoginBloc>(
create: (context) => LoginBloc(authCredentialHelper: authCredentialHelper),
child: LoginForm(authCredentialHelper: authCredentialHelper),
),
),
),
);
}
}
Getting this error:
The following assertion was thrown building _InheritedProviderScope<LoginBloc>(value: Instance of 'LoginBloc'):
BlocProvider.of() called with a context that does not contain a Cubit of type AuthenticationBloc.
No ancestor could be found starting from the context that was passed to BlocProvider.of<AuthenticationBloc>().
This can happen if the context you used comes from a widget above the BlocProvider.
Change this :
Navigator.push(context, MaterialPageRoute(builder: (context) {
return LoginScreen(authCredentialHelper: authCredentialHelper);
}));
to
Navigator.push(
context,
MaterialPageRoute(builder: (contextLoginScreen) {
return BlocProvider.value(
value: context.bloc<AuthenticationBloc>(),
child: LoginScreen(authCredentialHelper: authCredentialHelper));
}),
);
Stream builder is used to draw widget based on data from stream.
What is the right way to achieve navigation based on the data?
Details:
There is a logout button in drawer. It clears the session and emits a data in the stream.
There's a stateless widget with stream builder listening on data and updating UI. How to make it navigate to login screen based on data in the stream?
In your stateless widget's build method, you can listen changes in your stream with listen() method.
Widget build(BuildContext context) {
Repository.bulletins.listen((pet) {
pet.documents[pet.documents.length - 1].data['animalType'] == "Dog"
? Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LostPetForm(),
))
: print('not yet');
});
return Scaffold(...
Inspired from https://stackoverflow.com/a/54109955/1918649
In the build method of the widget that creates Profile
#override
Widget build(BuildContext context) {
final userBloc = BlocProvider.of<UserBloc>(context);
return ...
somewhere here Profile(userBloc)
...
}
class Profile extends StatefulWidget {
final userBloc;
Profile(this.userBloc);
#override
State<StatefulWidget> createState() => ProfileState();
}
class ProfileState extends State<Profile> {
#override
void initState() {
super.initState();
widget.userBloc.stream.listen((userData){
if(userData==null) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LandingPage(),
));
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
title: new Text("Profile"),
),
drawer: CustomDrawer(),
body: Center(
child: StreamBuilder<UserModel>(
initialData: widget.userBloc.user,
stream: widget.userBloc.stream,
builder: (ctx, snap) => snap.hasData?Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.network(snap.data?.imageUrl),
Text(snap.data?.username)
],
):Text('You are logged out'),
),
),
);
}
}
I want to navigate to the login page if there is no logged in user, otherwise display the homepage. I thought of calling Navigator.of(context).push() conditionally inside the build method but that triggers an exception. Is there some method I'm missing that I can override?
Update to add the Homepage widget
class HomePage extends StatelessWidget {
final AppUser user;
const HomePage({Key key, this.user}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Rera Farm'),
actions: <Widget>[
PopupMenuButton(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry>[
PopupMenuItem(
child: ListTile(
title: Text('Settings'),
onTap: () {
Navigator.pop(context);
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context)
=> SettingsPage()
));
},
),
),
];
},
)
],
),
body: _buildBody(context));
}
And the container
class HomePageContainer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (BuildContext context, _ViewModel vm) {
return HomePage(
user: vm.user,
);
},
);
}
}
You need to either use a ternary in the onTap if you're using the settings button or, if you just want it to automatically send the user to the correct page when the app starts, you can put the ternary in the MyApp build method.
If you are using the settings button and just want it to pop back to the previous page if the person is not logged in then you can change NotLoggedIn() to a pop.
For some strange reason SO is refusing to post the code when it is properly formatted with four spaces, exactly as it asks, so I'm just going to make a gist.
https://gist.github.com/ScottS2017/3288c7e7e9a014430e56dd6be4c259ab
Here's how I end up doing it. I do the checks in the main method, so the user sees the splash screen set in manifest while those weird checks are made:
void main() {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences.getInstance().then((instance) {
_token = instance.getString("token");
final _loggedIn = _token != null && token != "";
runApp(MyApp(loggedIn: _loggedIn));
});
}
Then in your app add the parameters to switch:
class MyApp extends StatelessWidget {
final bool loggedIn;
MyApp({this.key, this.loggedIn});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: loggedIn ? HomePage() : LoginPage(),
);
}
}
You can also use Navigator.pushReplacement() if you need to do it below MyApp(). Just posting it here for future generations.