Flutter control tab programmatically via Provider - flutter

I am unable to figure out how I can listen to changes in a class that uses the ChangeNotifier class. More specifically, outside the widget build method. So far I've tried simply attaching listeners to the class like so:
final providerObj = SomeProvider();
void initState() {
providerObj.addListener(() {
print("Something happens");
});
}
But this does nothing. I don't know why. I got it working once, but afterwards it just died on me. Nothing gets triggered, when there is a change in the provider class' variable values.
Here's a snippet of the SomeProvider
class SomeProvider with ChangeNotifier {
int _tabIndex = 0;
List<Tab> _tabs = [];
Tab _searchTab = new Tab(id: 0, name: 'Search');
void searchItems(String searchWord) {
_searchTab.items.addAll(_items
.where((item) => product.item.contains(searchWord))
.toList());
setTabIndex(0);
}
void setTabIndex(int index) {
_tabIndex = index;
notifyListeners();
}
/// Get all products
List<Category> get tabs {
return [..._tabs];
}
int get tabIndex {
return _tabIndex;
}
What I'm trying to accomplish is simple: Control the selected tab, when the selected index is in the SomeProvider class. Every time a tab is pressed, the index value gets saved in the provider. When the value changes, it should trigger an animateTo() call for a TabController instance.
The reason for this is because I have a separate search widget, which needs to change the tab to a Search tab. I just have no idea how I can listen to changes in the provider class outside the build() method and change the TabController index there.
So, I have a basic TabBarView to which I give a TabController instance.
class _ProductsState extends State<Products> with TickerProviderStateMixin {
late TabController _tabController;
void initState() {
super.initState();
_tabController = TabController(
vsync: this,
length: 0,
initialIndex: 0,
);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _fetchTabs(context) async {
await Provider.of<SomeProvider>(context, listen: false)
.fetchTabs();
_tabController = TabController(
vsync: this,
length: Provider.of<SomeProvider>(context, listen: false)
.tabs
.length,
);
}
Widget build(BuildContext context) {
// There's a FutureBuilder here etc.
...
child: TabBarView(
controller: _tabController,
...
What I'm trying to do is something like:
void triggeredWhenStateChanges() {
_tabController.animateTo(Provider.of<SomeProvider>(context, listen: false).tabIndex);
}
Little bit off-topic: I've used React before (not React-Native) and with it you can use something called a hook which can perform side effects when a value change is detected. With the useEffect hook I can normally execute some actions when a value in the state changes. I don't know how I can do this with Flutter, if it can be done at all.

Related

How can i call my provider model into initState method

i have several widgets use my provider as a condition , and i need one call to access my provider to whole widget from init state instead of wrapping every widget into my provider and it's consumer
this is my provider
class ProviderForFiltter extends ChangeNotifier {
bool isFiltterrr = true ;
bool get isFiltter => isFiltterrr;
void changeStatus(bool status){
isFiltterrr = status;
notifyListeners();
}
}
this is my main.dart
class Myproject extends StatefulWidget {
const Myproject ({Key? key}) : super(key: key);
#override
_Myproject State createState() => _Myproject State();
}
class _Myproject State extends State<Myproject > {
#override
Widget build(BuildContext context) {
return
Provider(
create: (BuildContext context) {
return ProviderForFiltter();
},
child: const MaterialApp(
debugShowCheckedModeBanner: false,
home: WelcomeScreen()
),
),
);
}
}
this is my Stful Widget
ProviderForFiltter? isF ;
#override
void initState() {
super.initState();
// i tried this but it always give me errors that is isF null value
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
isF = context.read<ProviderForFiltter>();
});
// also itried this but it don't work
isF = Provider.of<ProviderForFiltter>(context, listen: false);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Text('change'),
)
}
}
in the fact i need to use it's bool value as condition into Consumer and change it
i hope any help guys
is better don't do use Provider in initState, but you can use Future.delayed
because you need context
#override
void initState() {
super.initState();
// i tried this but it always give me errors that is isF null value
Future.delayed(Duration(seconds: 1), () {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
isF = context.read<ProviderForFiltter>();
});
// also itried this but it don't work
isF = Provider.of<ProviderForFiltter>(context, listen: false);
});
}
providers need context, in order to access it for one time you should override didChangeDependencies
#override
void didChangeDependencies() {
super.didChangeDependencies();
///access provider here and update your state if needed,
///this will be called one time just before the build method
**isF = Provider.of<ProviderForFiltter>(context, listen: false);**
}
There are multiple ways to deal with this.
The first option which I use is to add a Post Frame Callback like so:
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
aProvider = Provider.of< aProvider >(context, listen: false);
});
Alternatively, you could override the didChangeDependencies method to get the provider value once initState has been called - remembering to set the listen value to false.
I was facing the same issue and regarding the documentation of provider this should be the answer.
"This likely happens because you are modifying the ChangeNotifier from
one of its descendants while the widget tree is building."
In my case i am calling an http api async where the future is stored inside the notifier. So i have to update like this and it is working.
initState() {
super.initState();
Future.microtask(() =>
context.read<MyNotifier>().fetchSomething(someValue);
);
}
The best way is to use like this (when there's no "external parameter".
class MyNotifier with ChangeNotifier {
MyNotifier() {
_fetchSomething();
}
Future<void> _fetchSomething() async {}
}
source : https://pub.dev/packages/provider
You can use a different method called didChangeDependencies to get the value from the provider after the initState method is called. Also, make sure to set the listen value to false.
#override
void didChangeDependencies() {
super.didChangeDependencies();
final filtterData = Provider.of<ProviderForFiltter>(context, listen: false);
}

How to setState() and not rebuild tabs in a TabBarView

I have this problem, i have a home page where it has tabs. I like when i switch tabs to make the TabBar show black the tab that is selected and also i want to change the color of the whole Scaffold. So i made also a custom controller and used it like this:
TabController _controller;
#override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: 5);
_controller.index = 1;
_controller.addListener(() {
if (!_controller.indexIsChanging) {
setState(() {
scaffoldColor = colors[_controller.index];
});
}
});
}
The thing is that in this way all of my tabs are going to be rebuild and this is very bad because i have heavy tasks in few of them.
I also have used AutomaticKeepAliveClientMixin in all of the tabs but it didn't fix the problem. By the way i used it like this:
class Tab1 extends StatefulWidget {
#override
_Tab1State createState() => _Tab1State();
}
class _Tab1State extends State<Tab1> with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
print("Tab 1 Has been built");
return Text("TAB 1");
}
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
}
If for heavy tasks you mean a Future, you should place it inside initState.
See this answer: How to load async Stream only one time in Flutter?.

Get InheritedWidget parameter in initState

i need some help understanding how to obtain data from inherited widget.
I usually get the parameter from my widget directly from the build method using
#override
Widget build(BuildContext context) {
//THIS METHOD
var data = StateContainer.of(context).data;
return Container(child:Text("${data.parameter}"));
}
But this method cant be called from initState since there is no buildContext yet.
I need in the initState method to have that parameter (i call my fetch from server in that and i need to pass that data to my function), so, how should i do it?
#override
void initState() {
otherData = fetchData(data);
super.initState();
}
I tried using didChangeDipendencies() but it is called every time the view is rebuilt (popping from screen, etc.) so it is not what i want to use and neither the FutureBuilder widget.
Any suggestion?
First, note that you probably do want to use didChangeDependencies. But you can't just do your call there without any check. You need to wrap it in an if first.
A typical didChangeDependencies implementation should look similar to:
Foo foo;
#override
void didChangeDependencies() {
super.didChangeDependencies();
final foo = Foo.of(context);
if (this.foo != foo) {
this.foo = foo;
foo.doSomething();
}
}
Using such code, doSomething will be executed only when foo changes.
Alternatively, if you are lazy and know for sure that your object will never ever change, there's another solution.
To obtain an InheritedWidget, the method typically used is:
BuildContext context;
InheritedWidget foo = context.inheritFromWidgetOfExactType(Foo);
and it is this method that cannot be called inside initState.
But there's another method that does the same thing:
BuildContext context;
InheritedWidget foo = context.ancestorInheritedElementForWidgetOfExactType(Foo)?.widget;
The twist is:
- this method can be called inside initState
- it won't handle the scenario where the value changed.
So if your value never changes, you can use that instead.
1, If you only need InheritedWidget as a Provider of parameter for Widget.
You can using on initState as bellow:
#override
void initState() {
super.initState();
var data = context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
}
2, If you need listener to re-render widget when data of InheritedWidget change. I suggest you wrapper your StatefulWidget insider a StatelessWidget,
parameter of StatefulWidget is passed from StatelessWidget, when InheritedWidget change data, it will notify to StatelessWidget, on StatefulWidget we will get change on didChangeDependencies and you can refresh data.
This is code guide:
class WrapperDemoWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
DemoData data = StateContainer.of(context).data;
return Container();
}
}
class ImplementWidget extends StatefulWidget {
DemoData data;
ImplementWidget({this.data});
#override
_ImplementWidgetState createState() => _ImplementWidgetState();
}
class _ImplementWidgetState extends State<ImplementWidget> {
#override
void initState() {
super.initState();
//TODO Do sth with widget.data
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
//TODO Do change with widget.data
}
#override
Widget build(BuildContext context) {
return Container();
}
}
I prefer the solution with didChangeDependencies because Future.delayed solution is a bit hack, looks unprofessional and unhealthy. However, it works out of the box.
This is the solution I prefer:
class _MyAppState extends State<MyApp> {
bool isDataLoaded = false;
#override
void didChangeDependencies() {
if (!isDataLoaded) {
otherData = fetchData(data).then((_){
this.isDataLoaded = true;
});
}
super.didChangeDependencies();
}
...
You can also get the context in initState, try using a future with duration zero. You can find some examples here
void initState() {
super.initState();
Future.delayed(Duration.zero,() {
//use context here
showDialog(context: context, builder: (context) => AlertDialog(
content: Column(
children: <Widget>[
Text('#todo')
],
),
actions: <Widget>[
FlatButton(onPressed: (){
Navigator.pop(context);
}, child: Text('OK')),
],
));
});
}
i use it to make loading screens using inherited widgets and avoid some global variables

Streambuilder only fire once

I am having a problem where my stream builder is only firing once.
I am trying to configure my bottomNavigationBar to be of a different colour based on the theme selected by the user.
To do this, I have a page whereby the user can decide whether to use the light theme or dark theme. This is saved into the device while shared preferences and then using async, i will stream the current value into my bottomNavigationBar.
The problem occurs when i use a stream builder to create two if statement. Stating that if the value returned from the stream is 0, i will show a "light mode" bottom navigation bar. Else if its 1, i will show a dark theme.
All is well when i run the program for the first time. However upon navigation into the settings page and changing the user preference, the stream builder will not load again. Here are some snapshots of my code
I have tried removing the dispose method whereby the stream will close. However that didn't solve the problem.
The Stream Builder
class mainPagev2 extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _mainPageV2();
}
}
class _mainPageV2 extends State<mainPagev2>
with SingleTickerProviderStateMixin {
// TabController _tabController;
StreamController<int> streamController = new StreamController.broadcast();
#override
void initState() {
super.initState();
// _tabController = TabController(vsync: this, length: _pageList.length);
Stream<int> stream = new Stream.fromFuture(readCurrentTheme());
streamController.addStream(stream);
}
#override
void dispose() {
// _tabController.dispose();
super.dispose();
}
String currentColor = "#ab3334";
#override
Widget build(BuildContext context) {
// TODO: implement build
return StreamBuilder(
stream: streamController.stream,
builder: (context, asyncSnapshot) {
print(asyncSnapshot.data.toString() + "WHssssAT IS THIS");
if (asyncSnapshot.hasData) {
print(asyncSnapshot.error);
if (asyncSnapshot.data == 0) {
//Return light themed Container
currentColor = "#ffffff";
return ThemeContainer(color: currentColor );
} else {
currentColor = "#101424";
//Return dark themed Container
return ThemeContainer(color: currentColor );
}
} else {
//return dark themed
return ThemeContainer(color:currentColor);
}
},
);
//
}
}
Async Code to retrieve the value stored
Future<int> readCurrentTheme() async {
final prefs = await SharedPreferences.getInstance();
final key = 'themeMode';
final value = prefs.getInt(key) ?? 0;
print('read: $value LOOK AT THISSS');
return value;
}
It is expected that the stream builder will fire whenever the value stored is changed!
I don't see in your code a way to read data from SharedPreferences when the value stored is changed. You are effectively reading it once, so the StreamBuilder is only firering once. That makes sense.
To be able to do what you want, you have to use something to tell you widget that a state has changed elsewhere in the application. There a multiple ways to achieve this and I won't make the choice for you as it would be opinion based, so you can check thing like BloC, Provider, ScopedModel, InheritedWidget

Listening to a variable change in flutter

I'm trying to listen to a variable change to execute some code. So the variable is a bool named reset. I want to execute something (say reset the animation controller) once the animation ends OR a button (from another widget) is pressed. Executing something when the animation ends works as once it ends AnimationStatus.dismissed will be its state and the listener will be called. This then allows me to use a callback function onCountdownexpire in order to set the variable reset accordingly and based on what it is set, execute some code in the if(widget.reset) block. So there is no need to listen for this case.
Problem:
However, lets say a button is pressed (in another widget) and I set the variable reset to true. I want the animation to stop and reset. I cannot now depend on the AnimationStatus listener as it only executes when there is a state change. I want it to reset during its state. So I have to somehow listen to the variable widget.reset.
I have done some research on this and found out that ValueNotifier might be a way to go but there are not a lot of examples on how to go about using it. Like how do I go about listening to it ?
Code:
class Countdown extends StatefulWidget {
final VoidCallback onCountdownExpire;
bool reset;
Countdown(this.onCountdownExpire);
#override
CountdownState createState() => CountdownState();
}
class CountdownState extends State<Countdown> with TickerProviderStateMixin {
AnimationController controller;
String get timerString {
Duration duration = controller.duration * controller.value;
return '${duration.inMinutes}:${(duration.inSeconds % 60).toString()}';
}
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..addStatusListener((AnimationStatus status){
if (status == AnimationStatus.dismissed) {
debugPrint("Animation.dismissed");
widget.onCountdownExpire();
if (widget.reset) {
widget.reset = false;
controller.reverse(from: 1.0);
}
}
});
controller.reverse(from: 1.0);
}
... // omitted code
}
What I have tried but it does not seem to be working as expected:
class Countdown extends StatefulWidget {
final VoidCallback onCountdownExpire;
Countdown(this.onCountdownExpire);
ValueNotifier reset = ValueNotifier(false);
#override
CountdownState createState() => CountdownState();
}
class CountdownState extends State<Countdown> with TickerProviderStateMixin {
AnimationController controller;
#override
void initState() {
widget.reset.addListener(() {
debugPrint("value notifier is true");
controller.reverse(from: 1.0);
});
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..addStatusListener((AnimationStatus status){
if (status == AnimationStatus.dismissed) {
debugPrint("Animation.dismissed");
widget.onCountdownExpire();
}
});
controller.reverse(from: 1.0);
}
... // omitted code
}
Update: The solution (above) was actually working, I just had to use something like this to notify the listener:
countdown.reset.notifyListeners();
// OR
countdown.reset.value = true;
OP already got an answer in the comments. I'm just typing it here so the question is correctly marked as answered.
Just notify the listener:
countdown.reset.notifyListeners();
// OR
countdown.reset.value = true;
Credit to #pskink
Use of overwriting getter/setters
You can make use of the getter and setter functions.
In the example below we are printing happy birthday when the person's age changes:
class Person {
int _age;
Person(this._age);
int get age => _age;
set age(int newValue) {
print("Happy birthday! You have a new Age.");
//do some awsome things here when the age changes
_age = newValue;
}
}
The usage is exactly as you are used to:
final newPerson = Person(20);
print(newPerson.age); //20
newPerson.age = 21; //prints happy birthday
print(newPerson.age); //21