Rebuild the text of the outer class with Provider - flutter

The code below is a simplification of my project.
I want it make like: When you push the button, text is rebuilt and increase the number.
However, text isn't reloaded. but I detected that the number of count is actualy increased.
Why this text isn't reloaded? Some way to detect changes of other classes?
class CLS extends ChangeNotifier{
int count = 0;
void add(int i){
count += i;
notifyListeners();
}
}
....
class Main extends StatelessWidget{
Widget txt(){
return Consumer<CLS>(
builder: (context, value, _) => Text(value.count.ToString())
);
}
Widget but(int i){
return ElevatedButton(
child: Text("Button"),
onPressed: context.read<CLS>().add(i)
);
}
#override
Widget build(BuildContext context){
return Scaffold(
body: ChangeNotifierProvider(
create: (BuildContext context) => CLS(),
child: Column( children: [
txt(),
but(1)
]));
}
}

You are using same context for providing and accessing your Provider, so it can't find it. You should use new context to access your provided Provider, because it looks for it's ancessor to find provided Provider. And also you should use that new context to read your Provider from your but() method, like so:
Widget but(int i, BuildContext ctx) {
return ElevatedButton(
child: Text("Button"), onPressed: () => ctx.read<CLS>().add(i));
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ChangeNotifierProvider(
create: (BuildContext context) => CLS(),
child: Builder(builder: (ctx) { // new context(ctx) to find Provider from parent's(ancessor's) context
return Column(
children: [
txt(),
but(1, ctx), // use that new context in your but method for accessing Provider
],
);
}),
),
);
}

Related

Flutter: can't access Provider from showModalBottomSheet

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

BLOC losing context when navigating to a new page

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));
}),
);

Persist Provider data across multiple pages not working

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.

How to access Provided (Provider.of()) value inside showModalBottomSheet?

I have a FloatingActionButton inside a widget tree which has a BlocProvider from flutter_bloc. Something like this:
BlocProvider(
builder: (context) {
SomeBloc someBloc = SomeBloc();
someBloc.dispatch(SomeEvent());
return someBloc;
},
child: Scaffold(
body: ...
floatingActionButton: FloatingActionButton(
onPressed: _openFilterSchedule,
child: Icon(Icons.filter_list),
),
)
);
Which opens a modal bottom sheet:
void _openFilterSchedule() {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return TheBottomSheet();
},
);
}
I am trying to access SomeBloc using BlocProvider.of<SomeBloc>(context) inside TheBottomSheet but I get the following error:
BlocProvider.of() called with a context that does not contain a Bloc of type SomeBloc.
I have tried to use the solution described in https://stackoverflow.com/a/56533611/2457045 but only works for BottomSheet and not ModalBottomSheet.
Note: This is not restricted to BlocProvider or flutter_bloc. Any Provider from the provider package has the same behaviour.
How can I access BlocProvider.of<SomeBloc>(context) inside the showModalBottomSheet?
In case it's not possible to do that, how to adapt https://stackoverflow.com/a/56533611/2457045 solution to Modal Bottom Sheet?
InheritedWidgets, and therefore Providers, are scoped to the widget tree. They cannot be accessed outside of that tree.
The thing is, using showDialog and similar functions, the dialog is located in a different widget tree – which may not have access to the desired provider.
It is therefore necessary to add the desired providers in that new widget tree:
void myShowDialog() {
final myModel = Provider.of<MyModel>(context, listen: false);
showDialog(
context: context,
builder: (_) {
return Provider.value(value: myModel, child: SomeDialog());
},
);
}
Provider in showModalBottomSheet (Bottom-Sheet)
void myBottomSheet() {
final myModel = Provider.of<MyModel>(context, listen: false);
showModalBottomShee(
context: context,
builder: (_) {
return ListenableProvider.value(
value: myModel,
child: Text(myModel.txtValue),
);
},
);
}
You need move Provider to top layer(MaterialApp)
According to picture, Dialog widget is under MaterialApp, so this is why you using wrong context
wrap your whole child widget inside the consumer.
void myShowDialog() {
showDialog(
context: context,
builder: Consumer<MyModel>(
builder: (context, value, builder) {
retuen widget();
}
);
}
You should split Scaffold widget and its children, to another StatefulWidget
From single Widget
class MainScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
builder: (context) {
SomeBloc someBloc = SomeBloc();
someBloc.dispatch(SomeEvent());
return someBloc;
},
child: Scaffold(
body: ...
floatingActionButton: FloatingActionButton(
onPressed: _openFilterSchedule,
child: Icon(Icons.filter_list),
),
)
);
}
}
Splitted into these two widget
class MainScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
builder: (context) {
SomeBloc someBloc = SomeBloc();
someBloc.dispatch(SomeEvent());
return someBloc;
},
child: Screen(),
);
}
}
and ..
class Screen extends StatelessWidget {
void _openFilterSchedule() {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return TheBottomSheet();
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ...
floatingActionButton: FloatingActionButton(
onPressed: _openFilterSchedule,
child: Icon(Icons.filter_list),
),
);
}
}
I found a solution, Just return your showModalBottomSheet with a StatefulBuilder and use the context of your modalsheet builder to pass to your provider. a snippet of my code below:
Future<Widget> showModal(int qty, Product product) async {
return await showModalBottomSheet(
isScrollControlled: true,
backgroundColor: Colors.transparent,
context: context,
builder: (BuildContext ctx) {
return StatefulBuilder(builder: (ctx, state) {
return Container(
child: RaisedButton(
onPressed: () {
Product prod = Product(product.id,
product.sku, product.name, qty);
Provider.of<CartProvider>(ctx, listen:
false).addCart(prod);}),);
}
}
);
}
TLDR: Make sure your import statement's casings match your project's folder casings.
I came across one other quirk while debugging this same error. I had several providers that were all working, including in showModalBottomSheets, however one was not working. After combing through the entire widget tree, without finding any discrepancies, I found that I had capitalized the first letter of a folder on one of the import statements of my problem-child notifier. I think this confused the compiler and caused it to throw the Could not find the correct Provider above this widget error.
After ensuring the import statement casing matched the folder name, my provider problems were resolved. Hopefully this will save someone a headache.
Not finding a clear explanation of adding multiple provided values, I thought I'd share here for reference.
await showMobileModals(
isDismissible: false,
context: context,
child: MultiProvider(
providers: [
Provider.value(
value: provided_one,
),
Provider.value(
value: provided_two,
),
Provider.value(
value: provided_three,
),
],
child: Container(),
),
);
Faced the same issue while dealing with showModelBottomSheet, since it happens to work in a different (context)widget tree I had to level up my state to that of the app so that I could access my provider using the context.

Navigate based on data from stream

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'),
),
),
);
}
}