How to calculate page stay duration in Flutter? - 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?

Related

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?

Initializing riverpod Provider with a custom ChangeNotifier

I'm just trying out the new river_pod, flutter state management library. My goal here is simple. GestureDetector in the main page listens to vertical drags and updates the animation controller accordingly. And I'd like to listen to this animation somewhere else. I have written the following code, and it's working as expected. But I don't feel like I'm initializing the provider in the right way.
// a custom notifier class
class AnimationNotifier extends ChangeNotifier {
final AnimationController _animationController;
AnimationNotifier(this._animationController) {
_animationController.addListener(_onAnimationControllerChanged);
}
void forward() => _animationController.forward();
void reverse() => _animationController.reverse();
void _onAnimationControllerChanged() {
notifyListeners();
}
#override
void dispose() {
_animationController.removeListener(_onAnimationControllerChanged);
super.dispose();
}
double get value => _animationController.value;
}
// provider variable, (not initialized here)
var animationProvider;
// main Widget
class GestureControlledAnimationDemo extends StatefulWidget {
#override
_GestureControlledAnimationDemoState createState() =>
_GestureControlledAnimationDemoState();
}
class _GestureControlledAnimationDemoState
extends State<GestureControlledAnimationDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
double get maxHeight => 420.0;
#override
void initState() {
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
// provider is initialized here
animationProvider = ChangeNotifierProvider((_) {
return AnimationNotifier(_controller);
});
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return CustomScaffold(
title: 'GestureControlled',
body: GestureDetector(
onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: _handleDragEnd,
child: Container(
color: Colors.red,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Yo',
style: TextStyle(color: Colors.white),
),
NotifierTest(),
],
),
),
),
),
);
}
void _handleDragUpdate(DragUpdateDetails details) {
_controller.value -= details.primaryDelta / maxHeight;
}
void _handleDragEnd(DragEndDetails details) {
if (_controller.isAnimating ||
_controller.status == AnimationStatus.completed) return;
final double flingVelocity =
details.velocity.pixelsPerSecond.dy / maxHeight;
if (flingVelocity < 0.0) {
_controller.fling(velocity: max(2.0, -flingVelocity));
} else if (flingVelocity > 0.0) {
_controller.fling(velocity: min(-2.0, -flingVelocity));
} else {
_controller.fling(velocity: _controller.value < 0.5 ? -2.0 : 2.0);
}
}
}
// Widget which uses the provider
class NotifierTest extends HookWidget {
#override
Widget build(BuildContext context) {
final animationNotifier = useProvider(animationProvider);
double count = animationNotifier.value * 1000.0;
return Container(
child: Text(
'${count.floor()}',
style: TextStyle(color: Colors.white),
),
);
}
}
Since an animation controller instance is required to create an instance of AnimationNotifier, this can be done only after _controller initialization. So in the initState(), I've initialized both _controller and animationProvider. Is this the right way to use riverpod Provider?
If not, what modifications can be made?
First off I would highly recommend using hooks - it would reduce the boilerplate of your code significantly, for example, your class declaration will turn into:
class GestureControlledAnimationDemo extends HookWidget {
double get maxHeight => 420.0;
#override
Widget build(BuildContext context) {
final _controller = useAnimationController(duration: Duration(seconds: 1));
...
}
This removes the need for initState, dispose, etc.
Second, you don't necessarily want to create non-static providers inside classes. Instead, you could create it in global scope, or in this case, it makes sense to add as a static member on your custom notifier.
class AnimationNotifier extends ChangeNotifier {
...
static final provider = ChangeNotifierProvider((_) {
return AnimationNotifier(controller);
});
}
But wait, we don't have any variable named controller in this scope, so how do we get access? We can create a provider for an AnimationController, or we can turn your provider into a family so we can accept an AnimationController as a parameter. I will demonstrate the approach with families:
class AnimationNotifier extends ChangeNotifier {
...
static final provider = ChangeNotifierProvider.autoDispose.family<AnimationNotifier, AnimationController>((_, AnimationController controller) {
return AnimationNotifier(controller);
});
}
I added autoDispose as you likely want your controllers disposed when they are no longer needed. Now, we use the provider:
class GestureControlledAnimationDemo extends HookWidget {
double get maxHeight => 420.0;
#override
Widget build(BuildContext context) {
final controller = useAnimationController(duration: Duration(seconds: 1));
final provider = useProvider(AnimationNotifier.provider(controller));
...
}
If you do use hooks, make sure you change your riverpod dependency to hooks_riverpod.
EDIT:
It looks like for your use case you could potentially store the current controller in a StateProvider, then read it from the ChangeNotifierProvider instead of using families.
final controllerProvider = StateProvider<AnimationController>((_) => null);
class AnimationNotifier extends ChangeNotifier {
...
static final provider = ChangeNotifierProvider.autoDispose<AnimationNotifier>((ref) {
final controller = ref.read(controllerProvider)?.state;
return AnimationNotifier(controller);
});
}
class GestureControlledAnimationDemo extends HookWidget {
double get maxHeight => 420.0;
#override
Widget build(BuildContext context) {
final controller = useAnimationController(duration: Duration(seconds: 1));
final currentController = useProvider(controllerProvider);
currentController.state = controller;
final notifier = useProvider(AnimationNotifier.provider);
...
}
This should work. Note that when Riverpod 0.6.0 is released, you can also autodispose the StateProvider.

How update Widget when time is 23:59:59?

After doing an event..I want to show up my Text widget and change that widget when the time is at 23:59:59... so far I have done this code
DateFormat("HH:mm:ss").format(DateTime.now()) =="23:59:59"
? Text("After")
: Text("Before")
but the problem is... whenever I close my app and then re-open app... the widget doesn't change from Text("Before") to Text("After") although the time is already at 23:59:59... the widget only change after I am doing an event click and when app is still opened... is there a way to solve that problem without any additional event?
You have your Texts in build method. This method works only on widget's update, for example, if state changes (that's why widget updates when you do an event click, I think your widget rebuilds after click). But widget doesn't know anything about DateTime.now(). So you should put DateTime.now to state and update it once in second.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(16),
child: BeforeAfter(),
),
),
),
);
}
}
class BeforeAfter extends StatefulWidget {
#override
_BeforeAfterState createState() => _BeforeAfterState();
}
class _BeforeAfterState extends State<BeforeAfter> {
String time = DateFormat("HH:mm:ss").format(DateTime.now()); // our time which we will update
#override
void initState() {
super.initState();
updateTime();
}
void updateTime() {
Future.delayed(Duration(seconds: 1), () {
updateTime();
setState(() {
time = DateFormat("HH:mm:ss").format(DateTime.now()); // update time
});
});
}
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
time == "12:13:00" ? Text("After") : Text("Before"),
Text(time)
]); // compare time with some value
}
}
void main() {
runApp(MyApp());
}
Gif how it works

Stateful Widget not rebuilding after BLOC state change

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

Flutter: possible to detect when a drawer is open?

Is it possible to detect when a Drawer is open so that we can run some routine to update its content?
A typical use case I have would be to display the number of followers, likers... and for this, I would need to poll the server to get this information, then to display it.
I tried to implement a NavigatorObserver to catch the moment when the Drawer is made visible/hidden but the NavigatorObserver does not detect anything about the Drawer.
Here is the code linked to the NavigatorObserver:
import 'package:flutter/material.dart';
typedef void OnObservation(Route<dynamic> route, Route<dynamic> previousRoute);
typedef void OnStartGesture();
class NavigationObserver extends NavigatorObserver {
OnObservation onPushed;
OnObservation onPopped;
OnObservation onRemoved;
OnObservation onReplaced;
OnStartGesture onStartGesture;
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onPushed != null) {
onPushed(route, previousRoute);
}
}
#override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onPopped != null) {
onPopped(route, previousRoute);
}
}
#override
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onRemoved != null)
onRemoved(route, previousRoute);
}
#override
void didReplace({ Route<dynamic> oldRoute, Route<dynamic> newRoute }) {
if (onReplaced != null)
onReplaced(newRoute, oldRoute);
}
#override
void didStartUserGesture() {
if (onStartGesture != null){
onStartGesture();
}
}
}
and the initialization of this observer
void main(){
runApp(new MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
final NavigationObserver _observer = new NavigationObserver()
..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
print('** pushed route: $route');
}
..onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) {
print('** poped route: $route');
}
..onReplaced = (Route<dynamic> route, Route<dynamic> previousRoute) {
print('** replaced route: $route');
}
..onStartGesture = () {
print('** on start gesture');
};
#override
void initState(){
super.initState();
}
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Title',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SplashScreen(),
routes: <String, WidgetBuilder> {
'/splashscreen': (BuildContext context) => new SplashScreen(),
},
navigatorObservers: <NavigationObserver>[_observer],
);
}
}
Thanks for your help.
This answer is old now. Please see #dees91's answer.
Detecting & Running Functions When Drawer Is Opened / Closed
Run initState() when open drawer by any action.
Run dispose() when close drawer by any action.
class MyDrawer extends StatefulWidget {
#override
_MyDrawerState createState() => _MyDrawerState();
}
class _MyDrawerState extends State<MyDrawer> {
#override
void initState() {
super.initState();
print("open");
}
#override
void dispose() {
print("close");
super.dispose();
}
#override
Widget build(BuildContext context) {
return Drawer(
child: Column(
children: <Widget>[
Text("test1"),
Text("test2"),
Text("test3"),
],
),
);
}
}
State Management Considerations
If you are altering state with these functions to rebuild drawer items, you may encounter the error: Unhandled Exception: setState() or markNeedsBuild() called during build.
This can be handled by using the following two functions in initState() source
Option 1
WidgetsBinding.instance.addPostFrameCallback((_){
// Add Your Code here.
});
Option 2
SchedulerBinding.instance.addPostFrameCallback((_) {
// add your code here.
});
Full Example of Option 1
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// Your Code Here
});
}
As https://github.com/flutter/flutter/pull/67249 is already merged and published with Flutter 2.0 here is proper way to detect drawer open/close:
Scaffold(
onDrawerChanged: (isOpened) {
//todo what you need for left drawer
},
onEndDrawerChanged: (isOpened) {
//todo what you need for right drawer
},
)
Best solution
ScaffoldState has a useful method isDrawerOpen which provides the status of open/close.
Example: Here on the back press, it first checks if the drawer is open, if yes then first it will close before exit.
/// create a key for the scaffold in order to access it later.
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
#override
Widget build(context) {
return WillPopScope(
child: Scaffold(
// assign key (important)
key: _scaffoldKey,
drawer: SideNavigation(),
onWillPop: () async {
// drawer is open then first close it
if (_scaffoldKey.currentState.isDrawerOpen) {
Navigator.of(context).pop();
return false;
}
// we can now close the app.
return true;
});}
I think one simple solution is to override the leading property of your AppBar so you can have access when the menu icon is pressed an run your API calls based on that.
Yet I may have misunderstood your question because with the use case you provided, you usually need to manage it in a way that you can listen to any change which will update the value automatically so I am not sure what are you trying to trigger when the drawer is open.
Anyway here is the example.
class DrawerExample extends StatefulWidget {
#override
_DrawerExampleState createState() => new _DrawerExampleState();
}
class _DrawerExampleState extends State<DrawerExample> {
GlobalKey<ScaffoldState> _key = new GlobalKey<ScaffoldState>();
int _counter =0;
_handleDrawer(){
_key.currentState.openDrawer();
setState(() {
///DO MY API CALLS
_counter++;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
key: _key,
appBar: new AppBar(
title: new Text("Drawer Example"),
centerTitle: true,
leading: new IconButton(icon: new Icon(
Icons.menu
),onPressed:_handleDrawer,),
),
drawer: new Drawer(
child: new Center(
child: new Text(_counter.toString(),style: Theme.of(context).textTheme.display1,),
),
),
);
}
}
You can simply use onDrawerChanged for detecting if the drawer is opened or closed in the Scaffold widget.
Property :
{void Function(bool)? onDrawerChanged}
Type: void Function(bool)?
Optional callback that is called when the Scaffold.drawer is opened or closed.
Example :
#override
Widget build(BuildContext context) {
return Scaffold(
onDrawerChanged:(val){
if(val){
setState(() {
//foo bar;
});
}else{
setState(() {
//foo bar;
});
}
},
drawer: Drawer(
child: Container(
)
));
}
Unfortunately, at the moment there is no readymade solution.
You can use the dirty hack for this: to observe the visible position of the Drawer.
For example, I used this approach to synchronise the animation of the icon on the button and the location of the Drawer box.
The code that solves this problem you can see below:
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class DrawerListener extends StatefulWidget {
final Widget child;
final ValueChanged<FractionalOffset> onPositionChange;
DrawerListener({
#required this.child,
this.onPositionChange,
});
#override
_DrawerListenerState createState() => _DrawerListenerState();
}
class _DrawerListenerState extends State<DrawerListener> {
GlobalKey _drawerKey = GlobalKey();
int taskID;
Offset currentOffset;
#override
void initState() {
super.initState();
_postTask();
}
_postTask() {
taskID = SchedulerBinding.instance.scheduleFrameCallback((_) {
if (widget.onPositionChange != null) {
final RenderBox box = _drawerKey.currentContext?.findRenderObject();
if (box != null) {
Offset newOffset = box.globalToLocal(Offset.zero);
if (newOffset != currentOffset) {
currentOffset = newOffset;
widget.onPositionChange(
FractionalOffset.fromOffsetAndRect(
currentOffset,
Rect.fromLTRB(0, 0, box.size.width, box.size.height),
),
);
}
}
}
_postTask();
});
}
#override
void dispose() {
SchedulerBinding.instance.cancelFrameCallbackWithId(taskID);
if (widget.onPositionChange != null) {
widget.onPositionChange(FractionalOffset(1.0, 0));
}
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
key: _drawerKey,
child: widget.child,
);
}
}
If you are only interested in the final events of opening or closing the box, it is enough to call the callbacks in initState and dispose functions.
there is isDrawerOpen property in ScaffoldState so you can check whenever you want to check.
create a global key ;
GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
assign it to scaffold
Scaffold(
key: scaffoldKey,
appBar: ..)
check where ever in the app
bool opened =scaffoldKey.currentState.isDrawerOpen;
By the time this question was being posted it was a bit trick to accomplish this. But from Flutter 2.0, it is pretty easy. Inside your Scaffold you can detect both the right drawer and the left drawer as follows.
#override
Widget build(BuildContext context) {
return Scaffold(
onDrawerChanged: (isOpened) {
*//Left drawer, Your code here,*
},
onEndDrawerChanged: (isOpened) {
*//Right drawer, Your code here,*
},
);
}
You can use Scaffold.of(context) as below to detect the Drawer status :
NOTE: you must put your code in the Builder widget to use the context which contains scaffold.
Builder(
builder: (context) => IconButton(
icon: Icon(
Icons.menu,
color: getColor(context, opacity.value),
),
onPressed: () {
if (Scaffold.of(context).isDrawerOpen) {
Scaffold.of(context).closeDrawer();
} else {
Scaffold.of(context).openDrawer();
}
},
),
),