Widget not rebuilding after changeNotifier called - flutter

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

Related

How to use nested Consumers in 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...}),
]
);
}
}

Flutter Bloc: difference between RepositoryProvider and RepositoryProvider.value

I know the difference between RepositoryProvider and RepositoryProvider.value: the first one creates the repository for you and the second one receives a repository that is already created.
Please see the difference between the 2 code blocks - the first one is okay, the second one gives the following error.
RepositoryProvider.of() called with a context that does not contain a repository of type AuthRepository.
No ancestor could be found starting from the context that was passed to RepositoryProvider.of<AuthRepository>().
This can happen if the context you used comes from a widget above the RepositoryProvider.
The context used was: HomeScreen(dirty)
I don't understand why code 2 gives an error.
Code 1: success
class MyApp {
void main() {
// 1) Let the RepositoryProvider create the AuthRepository
runApp(RepositoryProvider(
crate: (context) => AuthRepository(),
child: BlocProvider(
create: (context) => AuthCubit(authRepository: RepositoryProvider.of<AuthRepository>(context)),
child: const MaterialApp(
// 2) Show HomeScreen that will access the AuthRepository instance
home: HomeScreen()
)
)
)
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final authCubit = BlocProvider.of<AuthCubit>(context);
// 3) This will succeed
final authRepo = RepositoryProvider.of<AuthRepository>(context);
return Scaffold(
body: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
return const Text(state.toString());
},
),
);
}
}
Code 2: error
class MyApp {
void main() {
// 1) Create a repository instance of AuthRepository
final authRepo = AuthRepository();
// 2) Add this AuthRepository instance to the RepositoryProvider.value
runApp(RepositoryProvider.value(
value: (context) => authRepo,
child: BlocProvider(
create: (context) => AuthCubit(authRepository: authRepo),
child: const MaterialApp(
// 3) Show HomeScreen that will access the AuthRepository instance
home: HomeScreen()
)
)
)
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final authCubit = BlocProvider.of<AuthCubit>(context);
// 4) This will fail
final authRepo = RepositoryProvider.of<AuthRepository>(context);
return Scaffold(
body: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
return const Text(state.authenticationStatus.toString());
},
),
);
}
}
The value should be a repository not a function.
Instead of
runApp(RepositoryProvider.value(
value: (context) => authRepo,
try
runApp(RepositoryProvider.value(
value: authRepo,

how to display the value that came from valueNotifier?

I have a valueNotifier that generates a list of events and takes a random string every 5 seconds and sends it to the screen. It lies in inheritedWidget. How can I display in the ListView the event that came with the valueNotifier? What is the correct way to print the answer?
My code:
class EventList extends StatelessWidget {
const EventList({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return EventInherited(
child: EventListScreen(),
);
}
}
class EventListScreen extends StatefulWidget {
const EventListScreen({Key? key}) : super(key: key);
#override
State<EventListScreen> createState() => _EventListScreenState();
}
class _EventListScreenState extends State<EventListScreen> {
#override
Widget build(BuildContext context) {
final eventNotifier = EventInherited.of(context).eventNotifier;
return Scaffold(
appBar: AppBar(
title: const Text('Event List'),
centerTitle: true,
),
body: Container(
padding: const EdgeInsets.all(30),
child: ValueListenableBuilder(
valueListenable: eventNotifier,
builder: (BuildContext context, List<String> value, Widget? child) {
return ListView(
children: [
],
);
},
),
),
);
}
}
class EventNotifier extends ValueNotifier<List<String>> {
EventNotifier(List<String> value) : super(value);
final List<String> events = ['add', 'delete', 'edit'];
final stream = Stream.periodic(const Duration(seconds: 5));
late final streamSub = stream.listen((event) {
value.add(
events[Random().nextInt(4)],
);
});
}
class EventInherited extends InheritedWidget {
final EventNotifier eventNotifier = EventNotifier([]);
EventInherited({required Widget child}) : super(child: child);
static EventInherited of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType()!;
}
#override
bool updateShouldNotify(EventInherited oldWidget) {
return oldWidget.eventNotifier.streamSub != eventNotifier.streamSub;
}
}
If you have correct value, you can return listview like this:
return ListView.builder(
itemCount: value.length,
itemBuilder: (context, index) {
return Text(value[index]);
},
);
After having a quick look at ValueNotifier,
It says the following:
When the value is replaced with something that is not equal to the old value as evaluated by the equality operator ==, this class notifies its listeners.
In your case, the value is an array. By adding items to the array, it wont recognise a change.
Also see other Stacko post.
Try something like:
value = [...value].add(...)

Flutter FutureProvider Execute build twice

I've changed from Statefulwidget using initState to fetch the data and Futurebuilder to load it to Futureprovider. But it seems like Futureprovider is execute build method twice, while my previous approach executed it once. Is this behaviour normal?
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg()
);
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
if (reportList == null) {
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Det finns inga rapporter."));
}
print(reportList.length);
return Container();
}
}
Im relativly new to flutter but I think its because StatelessWidget is #immutable, which means whenever something changes it needs to rebuild itself.
At first build there is async calling made and ReportWidg() is rendered.
Then this line final reportList = Provider.of<List<ReportModel>>(context); get new fetched data as result of async function therefore immutable widget needs to rebuild itself because it cannot be "changed".
In object-oriented and functional programming, an immutable object
(unchangeable object) is an object whose state cannot be modified
after it is created. ... This is in contrast to a mutable object
(changeable object), which can be modified after it is created
or am I wrong ?
I suspect your FutureProvider should be hoisted to a single instantiation, like placed into a global variable outside any build() methods. This will of course cache the result, so you can set it up for rebuild by having the value depend on other Providers being watch()ed or via FutureProvider.family.
You can copy paste run full code below
Yes. it's normal
First time Execute Build reportList is null and show CircularProgressIndicator()
Second time Execute Build reportList has data and show data
If you set listen: false , final reportList = Provider.of<List<ReportModel>>(context, listen: false);
You get only one Execute Build and the screen will always show CircularProgressIndicator()
In working demo simulate 5 seconds network delay so you can see CircularProgressIndicator() then show ListView
You can reference https://codetober.com/flutter-provider-examples/
code snippet
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
working demo
full code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ReportModel {
String title;
ReportModel({this.title});
}
class ReportsProvider with ChangeNotifier {
Future<List<ReportModel>> loadReportData(int no) async {
await Future.delayed(Duration(seconds: 5), () {});
return Future.value([
ReportModel(title: "1"),
ReportModel(title: "2"),
ReportModel(title: "3")
]);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ReportsPage(),
);
}
}
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg());
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
itemBuilder: (context, index) {
return Card(
elevation: 6.0,
child: Padding(
padding: const EdgeInsets.only(
top: 6.0, bottom: 6.0, left: 8.0, right: 8.0),
child: Text(reportList[index].title.toString()),
));
}),
);
}
}
In your case you should use Consumer, i.e.
FutureProvider<List<ReportModel>(
create: (_) => ...,
child: Consumer<List<ReportModel>(
builder: (_, reportList, __) {
return reportList == null ?
CircularProgressIndicator() :
ReportWidg(reportList);
}
),
),
But in this case you must to refactor your ReportWidg.

Combine two Streams into one StreamProvider

I have two streams:
Stream<FirebaseUser> FirebaseAuth.instance.onAuthStateChanged
Stream<User> userService.streamUser(String uid)
My userService requires the uid of the authenticated FirebaseUser as a parameter.
Since I will probably need to access the streamUser() stream in multiple parts of my app, I would like it to be a provider at the root of my project.
This is what my main.dart looks like:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
var auth = FirebaseAuth.instance;
var userService = new UserService();
return MultiProvider(
providers: [
Provider<UserService>.value(
value: userService,
),
],
child: MaterialApp(
home: StreamBuilder<FirebaseUser>(
stream: auth.onAuthStateChanged,
builder: (context, snapshot) {
if (!snapshot.hasData) return LoginPage();
return StreamProvider<User>.value(
value: userService.streamUser(snapshot.data.uid),
child: HomePage(),
);
}),
),
);
}
}
The issue is that when I navigate to a different page, everything below the MaterialApp is changed out and I lose the context with the StreamProvider.
Is there a way to add the StreamProvider to the MultiProvider providers-list?
Because when I try, I also have to create another onAuthStateChanged stream for the FirebaseUser and I don't know how to combine them into one Provider.
So this seems to work fine:
StreamProvider<User>.value(
value: auth.onAuthStateChanged.transform(
FlatMapStreamTransformer<FirebaseUser, User>(
(firebaseUser) => userService.streamUser(firebaseUser.uid),
),
),
),
If anybody has doubts about this in certain edge cases, please let me know.
Thanks to pskink for the hint about flatMap.
Maybe you can try this approach:
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<FirebaseUser>(
builder: (_) => FirebaseUser(),
),
],
child: AuthWidgetBuilder(builder: (context, userSnapshot) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.indigo),
home: AuthWidget(userSnapshot: userSnapshot),
);
}),
);
}
}
AuthWidgetBuilder.dart
Used to create user-dependant objects that need to be accessible by
all widgets. This widget should live above the [MaterialApp]. See
[AuthWidget], a descendant widget that consumes the snapshot generated
by this builder.
class AuthWidgetBuilder extends StatelessWidget {
const AuthWidgetBuilder({Key key, #required this.builder}) : super(key: key);
final Widget Function(BuildContext, AsyncSnapshot<User>) builder;
#override
Widget build(BuildContext context) {
final authService =
Provider.of<FirebaseUser>(context, listen: false);
return StreamBuilder<User>(
stream: authService.onAuthStateChanged,
builder: (context, snapshot) {
final User user = snapshot.data;
if (user != null) {
return MultiProvider(
providers: [
Provider<User>.value(value: user),
Provider<UserService>(
builder: (_) => UserService(uid: user.uid),
),
],
child: builder(context, snapshot),
);
}
return builder(context, snapshot);
},
);
}
}
AuthWidget.dart
Builds the signed-in or non signed-in UI, depending on the user
snapshot. This widget should be below the [MaterialApp]. An
[AuthWidgetBuilder] ancestor is required for this widget to work.
class AuthWidget extends StatelessWidget {
const AuthWidget({Key key, #required this.userSnapshot}) : super(key: key);
final AsyncSnapshot<User> userSnapshot;
#override
Widget build(BuildContext context) {
if (userSnapshot.connectionState == ConnectionState.active) {
return userSnapshot.hasData ? HomePage() : SignInPage();
}
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
This is originally from the tutorial of advance provider from Andrea Bizotto.
But I tailored some the code according to your your code above.
Hope this works, good luck!
Reference:
https://www.youtube.com/watch?v=B0QX2woHxaU&list=PLNnAcB93JKV-IarNvMKJv85nmr5nyZis8&index=5