Initializing riverpod Provider with a custom ChangeNotifier - flutter

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.

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

Best practice for calling an API or method at start point of Flutter App that needs context?

What is the best approach to calling an API or method when a screen wants to start in the Flutter app?
As I know in initState method, there is no context so I use didChangeDependencies method like below, but I always think there is a better way to tackle this issue. Any idea, snippet, or sample project link will be appreciated.
class HomeTabScreen extends StatefulWidget {
static String route = 'accountTab';
#override
_HomeTabScreenState createState() => _HomeTabScreenState();
}
class _HomeTabScreenState extends State<HomeTabScreen> {
late HomeTabViewModel homeTabVM;
var firstTime = true;
late TabController _controller;
#override
void initState() {
super.initState();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
if (firstTime) {
homeTabVM = context.watch<HomeTabViewModel>();
homeTabVM.getUserData(context);
_controller = TabController(
length: homeTabVM.list.length,
vsync: this,
initialIndex: 1,
);
firstTime = false;
}
homeTabVM.listKey = GlobalKey();
homeTabVM.listItems.clear();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.transparent,
child: Center(
child: CircularProgressIndicator(),
),
));
}
}

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

How to change all items on Column dependin on which one has been tapped

I'm not sure if how to do this.
I have a Column of AnimatedContainers. Initially all of them are 200 height. I want to implement some kind of callback, so when user tap on one item, this one becomes smaller(say 100 height) and the rest of the item in the list disappear. My AnimatedContainers are Stateful widgets
I guess I would have to use to callbacks, one for the columnn (parent) and the other to notify the children, but I don't know how to do this.
Summarised, what I have right now is
Stateless(Column(Stateful(List<AnimatedContainer>)))
If something is not clear please comment
Thanks
EDIT to add some code and more info
class SectionButton extends StatefulWidget {
double screenHeight;
Stream stream;
int index;
SectionButton(this.screenHeight, this.stream, this.index);
#override
_SectionButtonState createState() => _SectionButtonState();
}
class _SectionButtonState extends State<SectionButton> {
double height;
StreamSubscription streamSubscription;
initState() {
super.initState();
this.height = this.widget.screenHeight / n_buttons;
streamSubscription =
widget.stream.listen((_) => collapse(this.widget.index));
}
void collapse(int i) {
if (this.widget.index != i) {
setState(() {
this.height = 0;
});
} else {
setState(() {
this.height = this.widget.screenHeight / appBarFraction;
});
}
}
#override
dispose() {
super.dispose();
streamSubscription.cancel();
}
#override
Widget build(BuildContext context) {
return AnimatedContainer(
height: this.height,
duration: Duration(milliseconds: 300),
);
}
}
class HomeScreen extends StatelessWidget {
List<Widget> sections = [];
bool areCollapsed = false;
final changeNotifier = new StreamController.broadcast();
void createSections(screenHeight) {
for (var i = 0; i < buttonNames.length; i++) {
this.sections.add(GestureDetector(onTap:(){
print("Section i was tapped");
changeNotifier.sink.add(i);}, //THIS IS NOT WORKING
child: SectionButton(screenHeight, changeNotifier.stream, i),),);
}
}
#override
Widget build(BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
double screenHeight =
mediaQueryData.size.height - mediaQueryData.padding.vertical;
createSections(screenHeight);
return SafeArea(
child: SizedBox.expand(
child: Column(children: this.sections)
),
);
}
}
BTW what I'm trying to implement is something like this:
You have to store your height in a variable and wrap your widget with GestureDetector
GestureDetector(
onTap: () {
setState(() { _height = 100.0; });
},
child: Container(
child: Text('Click Me'),
),
)
Alternatively, you can also use AnimatedSize Widget.
Animated widget that automatically transitions its size over a given
duration whenever the given child's size changes.