How to use nested Consumers in Flutter? - flutter

I need to access two different view model in one page in Flutter.
How to use nested Consumers in Flutter, Will they effect each other?
How can I use it in this code for example?
class CounterDisplay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<CounterModel>(
builder: (context, counterModel, child) {
return Text('${counterModel.count}');
},
);
}
}

you can use Consumer2<> to access two different providers like this :
class CounterDisplay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer2<CounterModel, SecondModel>(
builder: (context, counterModel, secondModel, child) {
return Text('${counterModel.count}');
},
);
}
}
With this, your Text() widget will be rebuilt each time one the providers value is changed with notifyListener().
If your Text() widget doesn't need to be rebuilt with one of your providers, you can simply use Provider.of<MySecondProvider>(context, listen: false);.
Here for example:
class CounterDisplay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<CounterModel>(
builder: (context, counterModel, child) {
MyThemeProvider myThemeProvider = Provider.of<MyThemeProvider>(context, listen: false);
return Text('${counterModel.count}', color: myThemeProvider.isDark ? Colors.white : Colors.dark);
},
);
}
}
I hope this helps!

You can absolutely nest consumers, but you need to understand if you really want them to nest.
Case 1: Nesting consumers:
class CounterDisplay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<CounterModel>(
builder: (context, counterModel, child) {
return Consumer<SomeAnotherModel>(
builder: (anothercontext,anotherCounterModel, anotherChild) {
return Text('${counterModel.count}');
});
},
);
}
}
Please note that if consumer for CounterModel is rebuilt, everything will be rebuilt. If consumer for SomeAnotherModel is rebuild, only the part inside its builder would be rebuilt.
Instead of using consumers this way, I'd recommend them to be used as in case 2 below.
Case 2: Use consumers on the required components:
class CounterDisplay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
children:[
Consumer<CounterModel>(
builder: (context, counterModel, child) { return...}),
Consumer<CounterModel>(
builder: (anotherContext, anotherModel, anotherChild) { return...}),
]
);
}
}

Related

Widget not rebuilding after changeNotifier called

I have a provider with an int variable currentPage that defines the initial page of a PageView. I have this because I want to change the currentPage with widgets that far under the tree, or descendent widgets. I've set up everything correctly, but when changeNotifier is called, the page doesn't change.
Here's the provider class-
class CurrentPageProvider with ChangeNotifier{
int? currentPage;
CurrentPageProvider({this.currentPage});
changeCurrentPage(int page) {
currentPage = page;
notifyListeners();
}
}
To use it, I've wrapped my MaterialWidget with a MultiProvider as such-
class Test extends StatelessWidget {
const Test({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => CurrentPageProvider(currentPage: 0))
],
child: MaterialApp(
title: "Test",
debugShowCheckedModeBanner: false,
theme: ThemeData.light().copyWith(
primaryColor: yellowColor,
),
home: const ResponsiveRoot(),
),
);
}
}
And here's the widget where the child should rebuild, but isn't-
class ResponsiveRoot extends StatelessWidget {
const ResponsiveRoot({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
int currentPage = Provider.of<CurrentPageProvider>(context).currentPage!;
print("CurrentPageUpdated");
return LayoutBuilder(
builder: ((context, constraints) {
if (constraints.maxWidth > kWebScreenWidth) {
return const WebscreenLayout();
} else { //The page view is here
return MobileScreenLayout(
currentPage: currentPage,
);
}
}),
);
}
}
Upon debugging, I've found out that "CurrentPageUdated" gets printed when I'm calling the changeCurrentPage. However, the initState of the MobileScreenLayout doesn't get called (This widget has the pageView)
How do I fix this? Thanks!
in order to update the state of the the app you need to use Consumer widget.
Consumer<Your_provider_class>(
builder: (BuildContext context, provider_instance, widget?){
},
child: any_widget, but not neccessary,
)
The problem seems to be that even though your Provider.of mechanism needs to listen to changes, it does not.
What you can do is, do the recommended way on the documentation and you can either use the watch extension function or use Consumer or Selector widgets.
Here is an example on how to do it with your example with a Selector.
For more information read about Selector here
class ResponsiveRoot extends StatelessWidget {
const ResponsiveRoot({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Selector<CurrentPageProvider, int>(
selector: (context, provider) => provider.currentPage!,
builder: (context, currentPage, child) {
print("CurrentPageUpdated");
return LayoutBuilder(
builder: ((context, constraints) {
if (constraints.maxWidth > kWebScreenWidth) {
return const WebscreenLayout();
} else {
//The page view is here
return MobileScreenLayout(
currentPage: currentPage,
);
}
}),
);
},
);
}
}

Why rebuilding when TextField tapped?

If remove the MediaQuery.of(context).size code, it will not rebuild.
this is my code.
class ExamplePage extends StatelessWidget {
Future<Size> init(BuildContext context) async {
print("init");
return MediaQuery.of(context).size;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: init(context),
builder: (BuildContext context, AsyncSnapshot<Size> snapshot) {
return Center(child: TextField());
}));
}
}
future: init(context),
This will probably cause init(context) to be called each time build() is called for this widget. Instead, something like this should be done in the initState() for this widget, so that it is done only once at widget creation.

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

ChangeNotifierProvider inside ListView in flutter

class EventTimeModel with ChangeNotifier {
update() {
notifyListeners();
}
}
class SingleEvent extends StatelessWidget {
...
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context2) => event.timeModel,
child: buildEventColumn());
}
}
class EventList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: events.length,
itemBuilder: (context, index) {
Event event = events[index];
return SingleEvent(event: event)
}
}
EventList uses a ListView to display mutliple SingleEvents. SingleEvent uses a ChangeNotifierProvider to provide EventTimeModel. When scrolling up and down I got the message
Unhandled Exception: A EventTimeModel was used after being disposed.
E/flutter (10215): Once you have called dispose() on a EventTimeModel, it can no longer be used.
So I think an event was deleted because it has been outside the screen. When it should be displayed again the error was thrown. How can I fix this?
Since timeModel exists the create method can't be used here.
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: event.timeModel,
child: buildEventColumn());
}

How to build a PreferredSizeWidget, not a Widget, from a FutureBuilder or async code?

I know how to build a Widget from a FutureBuilder, so this is my future/async function, for example:
Future<Null> myFuture() async
{
// etc.
}
If I want to build an AppBar title, this works fine:
class MyStuff extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
title: FutureBuilder(
future: myFuture(),
builder: (context, snapshot) {
// etc...
return Text("blahblah");
}
));
}
}
Now, I want to build the AppBar's bottom which expects a PreferredSizeWidget, so this works, but is not async:
class MyStuff2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
bottom: PreferredSize(),
);
}
}
But how can I use this in a future/async way? this doesn't even compile:
class MyStuff3 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
bottom: FutureBuilder(, // is there a FutureBuilder for PreferredSizeWidgets?
future: myFuture(),
builder: (context, snapshot) {
// etc...
// I want to compute preferred size from async call result
return PreferredSize(); // pseudo-code ???
}
);
}
}
It's not gonna work since the FutureBuilder returns a Widget , so, what you can do is convert your stateless builder to a stateful one and create your own "PreferredSizeFutureBuilder"
import 'package:flutter/material.dart';
class PreferredSizeFutureBuilder extends StatefulWidget {
#override
_PreferredSizeFutureBuilderState createState() =>
_PreferredSizeFutureBuilderState();
}
class _PreferredSizeFutureBuilderState
extends State<PreferredSizeFutureBuilder> {
bool isCompleted = false;
#override
void initState() {
super.initState();
myFuture();
}
Future<void> myFuture() async {
//do something
setState(() {
isCompleted = true;
});
}
#override
Widget build(BuildContext context) {
return AppBar(
bottom: isCompleted //we check if the future has ended
? PreferredSize(
child: YourComponent(), //your component
preferredSize: Size.fromHeight(12.0), //the size you want
)
: null); //if the future is loading we don't return anything, you can add your loading widget here
}
}
Of course this means that the snapshot object is not gonna work , instead you use the isCompleted bool to check if your future is done, you can even use the ConnectionState instead of a boolean to have the "snapshot" behaviour
You can wrap FutureBuilder into a PreferredSize-Widget.
No need to create an own FutureBuilder-Widget that implements PreferredSize.
class MyStuff2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
bottom: PreferredSize(
child: FutureBuilder(future: _getData(), builder: (BuildContext context, AsyncSnapshot snapshot) {
if(snapshot.data != null) {
return Text(snapshot.data);
} else {
return Text('Loading...');
}
),
preferredSize: Size.fromHeight(50.0),
);
}
}