Stateful Widget not rebuilding after BLOC state change - flutter

I'm having trouble to understend why my Stateful widget is not updating the state after a rebuild. I have a stateful widget responsable for decrementing a counter each second, so it recieves a initial value, I pass this initial value to the State and start decrementing it.
Also it has a button that when pressed sends a event to my bloc which rebuilds the stateful widget with a new initial value, the thing is, it indeed sends and update the initial value, but the counter continue decrementing the old value and I have no clue why. Here a example of the code:
Widget
void main() => runApp(
BlocProvider(
create: (context) => TBloc(),
child: MaterialApp(
home: Scaffold(
body: BlocBuilder<TBloc, BState>(
builder: (context, state) {
if (state is BState) {
return Column(
children: [
Counter(state.value),
FlatButton(
child: Text("tap"),
onPressed: () =>
BlocProvider.of<TBloc>(context).add(BEvent(200)),
),
],
);
}
return Container();
},
),
),
),
),
);
class Counter extends StatefulWidget {
final int initialValue;
Counter(this.initialValue);
#override
CounterState createState() => CounterState(this.initialValue);
}
class CounterState extends State<Counter> {
Timer timer;
int value;
CounterState(this.value);
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() => --value);
});
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Value: $value"),
Text("Initial: ${widget.initialValue}")
],
);
}
}
Bloc
class TBloc extends Bloc<BEvent, BState> {
#override
BState get initialState => BState(100);
#override
Stream<BState> mapEventToState(BEvent event) async* {
yield BState(event.value);
}
}
class BEvent extends Equatable {
final int value;
BEvent(this.value);
#override
List<Object> get props => [value];
}
class BState extends Equatable {
final int value;
BState(this.value);
#override
List<Object> get props => [value];
}
Thank you in advance.

That's because you are only setting your Timer on your Counter's initState which is called only once per widget lifecycle.
Basically, when your Counter is first built, it will call initState() and as long as it remains in that widget tree, only didUpdateWidget() and/or didChangeDependencies() will be called in order to update that widget (eg. when rebuilt with new parameters, just like you're doing).
The same happens for dispose() method, which is the opposite of initState() called only once but this time, when the widget is being removed from the tree.
Also, you are using states the wrong way here. You don't need to pass the parameters to the state itself (like you are doing by:
CounterState createState() => CounterState(this.initialValue);
but instead, you can access all parameters from that widget by calling: widget.initialValue.
So, with all of that said, basically, what you'll want to do is to update your Counter widget based on the new updates, like so:
class Counter extends StatefulWidget {
final int initialValue;
Counter(this.initialValue);
#override
CounterState createState() => CounterState();
}
class CounterState extends State<Counter> {
Timer timer;
int value;
void _setTimer() {
value = widget.initialValue;
timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() => --value);
});
}
#override
void initState() {
super.initState();
_setTimer();
}
#override
void didUpdateWidget(Counter oldWidget) {
super.didUpdateWidget(oldWidget);
if(oldWidget.initialValue != widget.initialValue) {
_setTimer();
}
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Value: $value"),
Text("Initial: ${widget.initialValue}")
],
);
}
}

Related

How to calculate page stay duration in Flutter?

I want to calculate every pages stay duration of my flutter app. The stay duration means from page becomes visible to page hide.
How do i do this?
Any idea might help me.
You can use this wrapper widget,
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: PageView.builder(
itemCount: 4,
itemBuilder: (context, index) => TrackWidgetLifeTime(
child: Text("$index"),
lifeTime: (spent) {
log("I've spend ${spent.toString()}");
},
),
),
),
);
}
}
class TrackWidgetLifeTime extends StatefulWidget {
const TrackWidgetLifeTime({super.key, this.lifeTime, required this.child});
final Function(Duration spent)? lifeTime;
final Widget child;
#override
State<TrackWidgetLifeTime> createState() => _TrackWidgetLifeTimeState();
}
class _TrackWidgetLifeTimeState extends State<TrackWidgetLifeTime> {
var stopwatch = Stopwatch();
#override
void initState() {
super.initState();
stopwatch.start();
}
#override
void dispose() {
if (widget.lifeTime != null) widget.lifeTime!(stopwatch.elapsed);
super.dispose();
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
An idea would be to capture the time at initState for stateful widgets, or under build method for stateless widgets, and compare it with the time on leave, as follows:
late DateTime started;
DateTime? ended;
#override
void initState() {
started = DateTime.now();
super.initState();
}
void onLeave() {
ended = DateTime.now();
if (ended != null) {
var lapse = Duration(microseconds: ended!.microsecondsSinceEpoch-started.microsecondsSinceEpoch);
print("Viewed page ${lapse.inSeconds} seconds");
}
}
To learn how to detect when the user moves away from the current page take a look at this answer:
Detect if the user leaves the current page in Flutter?

How do I call method initState() from a stateless widget extending class?

I have a splash screen in my homepage activity which should then redirect to my second activity:
class _MyHomePageState extends State<MyHomePage> {
#override
void initState(){
super.initState();
Timer(const Duration(seconds: 3),
()=>Navigator.pushReplacement(context,
MaterialPageRoute(builder:
(context) =>
SecondScreen()
)
)
);
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child:FlutterLogo(size:MediaQuery.of(context).size.height)
);
}
}
class SecondScreen extends StatelessWidget { //checking if internet connection exists here
late StreamSubscription subscription;
var isDeviceConnected = false;
bool isAlertSet = false;
#override
void initState(){
getConnectivity();
super.initState(); //initState() is undefined
}
getConnectivity() =>
subscription = Connectivity().onConnectivityChanged.listen(
(ConnectivityResult result) async {
isDeviceConnected = await InternetConnectionChecker().hasConnection;
if (!isDeviceConnected && isAlertSet == false) {
showDialogBox();
setState(() => isAlertSet = true); //setState() is undefined
}
},
);
#override
void dispose() {
subscription.cancel();
super.dispose(); //dispose() is undefined
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment:MainAxisAlignment.center,
children:[
Image(
image: const AssetImage('images/logo.png'),
height: AppBar().preferredSize.height,),
const SizedBox(
width: 15,
),
Text(
widget.title
),
]
)
)
);
}
showDialogBox() => showCupertinoDialog<String>(
context: context,
builder: (BuildContext context) => CupertinoAlertDialog(
title: const Text('No internet connection'),
content: const Text('Please make sure you have an active internet connection to continue'),
actions: <Widget>[
TextButton(
onPressed: () async {
Navigator.pop(context, 'Cancel');
setState(() => isAlertSet = false);
isDeviceConnected =
await InternetConnectionChecker().hasConnection;
if (!isDeviceConnected && isAlertSet == false) {
showDialogBox();
setState(() => isAlertSet = true);
}
},
child: const Text('OK'),
),
],
),
);
}
The flow is such that, in the homepage activity a splash screen will open and then it will redirect to the second activity which will check if the user has an active internet connection.
I tried changing the SecondScreen to statefulWidget, but I still keep getting the same error.
Stateless: A stateless widget is like a constant. It is immutable. If you want to change what is displayed by a stateless widget, you'll have to create a new one.
Stateful: Stateful widgets are the opposite. They are alive and can interact with the user. Stateful widgets have access to a method named setState, which basically says to the framework "Hello, I want to display something else. Can you redraw me please ?".
A stateless widget can only be drawn once when the Widget is loaded/built and cannot be redrawn based on any events or user actions.
This kind of widget has no state, so they can’t change according to an internal state, they only react to higher widget changes.
more information read this documentation StatefulWidget and StatelessWidget
convert in stateful widget
class SecondScreen extends StatefulWidget {
const SecondScreen({Key? key}) : super(key: key);
#override
State<SecondScreen> createState() => _SecondScreenState();
}
class _SecondScreenState extends State<SecondScreen> {
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
there is no initState in a stateless widget but you can call a function after rebuild of a stateless widget using this:
class SecondScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
WidgetsBinding.instance?.addPostFrameCallback((_) {
// do something
print("Build Completed");
});
return Container(
color: Colors.blue,
child: WhatEverWidget()
);
}
}

How to rebuild specified widgets only in flutter

When calling onClick(), how can I prevent only WidgetOne is rebuild, while WidgetTwo is not?
class SomeClass extends StatefulWidget {
#override
State<SomeClass> createState() => _SomeClass();
}
class _SomeClass extends State<SomeClass>{
int _i = 0;
#override
Widget build(BuildContext context) {
return child: Column(
children: [
WidgetOne(i: _i),
WidgetTwo()
],
)
}
void onClick() {
setState(() {
i++;
});
}
I add the following text, because otherwise stackoverflow is written "It looks like your post is mostly code; please add some more details." :(
A simple and fast solution is to put WidgetOne in StatefulBuilder like this:
class SomeClass extends StatefulWidget {
#override
State<SomeClass> createState() => _SomeClass();
}
class _SomeClass extends State<SomeClass> {
int _i = 0;
Function? innerSetState;
#override
Widget build(BuildContext context) {
return Column(
children: [
StatefulBuilder(
builder: (context, innerSetState) {
this.innerSetState = innerSetState;
return WidgetOne(i: _i);
},
),
WidgetTwo()
],
);
}
void onClick() {
innerSetState?.call(() {
_i++;
});
}
}
Another solution is to use provider state manager along with Consumer widget.

How to pass a GlobalKey through Stateless Widget Children

I'm trying to create a custom menu bar in my app. Right now, the biggest issue I'm having is passing a state for when it's expanded to it's children after a setState occurs.
I thought about inheritance, but from what I've tried all inheritance needs to be in-line. I can't create a widget where the children [] are fed into the constructor on an ad-hoc basis.
My current approach is to use a GlobalKey to update the State of the children widgets being inserted into the StateFul while updating them directly.
The children for my MenuBar are declared as:
List<MenuBarItem> menuItems;
MenuBarItem is an abstract interface class that I intend to use to limit the widgets that can be fed in as menuItems to my MenuBar.
abstract class iMenuItem extends Widget{}
class MenuBarItem extends StatefulWidget implements iMenuItem{
At some iterations of this script, I had a bool isExpanded as part of the iMenuItem, but determined it not necessary.
Here is my code at its current iteration:
My Main:
void main() {
// runApp(MainApp());
//runApp(InherApp());
runApp(MenuBarApp());
}
class MenuBarApp extends StatelessWidget{
#override
Widget build(BuildContext context){
return MaterialApp(
home: Scaffold(
body: MenuBar(
menuItems: [
// This one does NOT work and is where I'm trying to get the
// value to update after a setState
MenuBarItem(
myText: 'Outsider',
),
],
),
),
);
}
}
My Code:
import 'package:flutter/material.dart';
/// Primary widget to be used in the main()
class MenuBar extends StatefulWidget{
List<MenuBarItem> menuItems;
MenuBar({
required this.menuItems,
});
#override
State<MenuBar> createState() => MenuBarState();
}
class MenuBarState extends State<MenuBar>{
bool isExpanded = false;
late GlobalKey<MenuBarContainerState> menuBarContainerStateKey;
#override
void initState() {
super.initState();
menuBarContainerStateKey = GlobalKey();
}
#override
Widget build(BuildContext context){
return MenuBarContainer(
menuItems: widget.menuItems,
);
}
}
class MenuBarContainer extends StatefulWidget{
List<MenuBarItem> menuItems;
late Key key;
MenuBarContainer({
required this.menuItems,
key,
}):super(key: key);
#override
MenuBarContainerState createState() => MenuBarContainerState();
}
class MenuBarContainerState extends State<MenuBarContainer>{
bool isExpanded = false;
#override
void initState() {
super.initState();
isExpanded = false;
}
#override
Widget build(BuildContext context){
List<Widget> myChildren = [
ElevatedButton(
onPressed: (){
setState((){
this.isExpanded = !this.isExpanded;
});
},
child: Text('Push Me'),
),
// This one works. No surprise since it's in-line
MenuBarItem(isExpanded: this.isExpanded, myText: 'Built In'),
];
myChildren.addAll(widget.menuItems);
return Container(
child: Column(
children: myChildren,
),
);
}
}
/// The item that will appear as a child of MenuBar
/// Uses the iMenuItem to limit the children to those sharing
/// the iMenuItem abstract/interface
class MenuBarItem extends StatefulWidget implements iMenuItem{
bool isExpanded;
String myText;
MenuBarItem({
key,
this.isExpanded = false,
required this.myText,
}):super(key: key);
#override
State<MenuBarItem> createState() => MenuBarItemState();
}
class MenuBarItemState extends State<MenuBarItem>{
#override
Widget build(BuildContext context){
GlobalKey<MenuBarState> _menuBarState;
return Row(
children: <Widget> [
Text('Current Status:\t${widget.isExpanded}'),
Text('MenuBarState GlobalKey:\t${GlobalKey<MenuBarState>().currentState?.isExpanded ?? false}'),
Text(widget.myText),
],
);
}
}
/// To give a shared class to any children that might be used by MenuBar
abstract class iMenuItem extends Widget{
}
I've spent 3 days on this, so any help would be appreciated.
Thanks!!
I suggest using ChangeNotifier, ChangeNotifierProvider, Consumer and context.read to manage state. You have to add this package and this import: import 'package:provider/provider.dart';. The steps:
Set up a ChangeNotifier holding isExpanded value, with a setter that notifies listeners:
class MyNotifier with ChangeNotifier {
bool _isExpanded = false;
bool get isExpanded => _isExpanded;
set isExpanded(bool isExpanded) {
_isExpanded = isExpanded;
notifyListeners();
}
}
Insert the above as a ChangeNotifierProvider in your widget tree at MenuBar:
class MenuBarState extends State<MenuBar> {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyNotifier(),
child: MenuBarContainer(
menuItems: widget.menuItems,
));
}
}
After this you can easily read and write the isExpanded value from anywhere in your widget tree under the ChangeNotifierProvider, for example:
ElevatedButton(
onPressed: () {
setState(() {
final myNotifier = context.read<MyNotifier>();
myNotifier.isExpanded = !myNotifier.isExpanded;
});
},
child: Text('Push Me'),
),
And if you want to use this state to automatically build something when isExpanded is changed, use Consumer, which will be notified automatically upon every change, for example:
class MenuBarItemState extends State<MenuBarItem> {
#override
Widget build(BuildContext context) {
return Consumer<MyNotifier>(builder: (context, myNotifier, child) {
return Row(
children: <Widget>[
Text('Current Status:\t${myNotifier.isExpanded}'),
Text(widget.myText),
],
);
});
}
}

Will stateless widget be rebuilt again?

I am making a list of stateless widget as shown below and passing the id as the parameter to the widgets.
Code for cartPage:-
class Cart extends StatefulWidget {
#override
_CartState createState() => _CartState();
}
class _CartState extends State<Cart> {
bool loading=true;
List<CartTile> cartTiles=[];
#override
void initState() {
super.initState();
if(currentUser!=null)
getData();
}
getData()async
{
QuerySnapshot snapshot=await cartReference.doc(currentUser.id).collection('cartItems').limit(5).get();
snapshot.docs.forEach((doc) {
cartTiles.add(CartTile(id: doc.data()['id'],index: cartTiles.length,));
});
setState(() {
loading=false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: loading?Center(child:CircularProgressIndicator():SingleChildScrollView(
child: Column(
children: cartTiles,
),
),
);
}
}
Code for CartTile:-
class CartTile extends StatelessWidget {
final String id;
CartTile({this.id,});
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: productReference.doc(id).snapshots(),
builder: (context,snapshot)
{
//here am using the snapshot to build the cartTile.
},
);
}
}
So, my question is whenever I will call setState in my homepage then will the stateless widget be rebuilt and increase my document reads. Because i read somewhere that when we pass the same arguments or parameters to a stateless widget then due to its cache mechanism it doesn't re build. If it will increase my reads then is there any other way to solve this problem?