How to hot-reload fields of a State subclass in Flutter?
I know that the modifying the initial value of fields isn't taken into account during hot-reload and that I can use hot-restart for them. But this is painfully slow.
Is there any way to ease the process?
A typical use-case would be animations, especially AnimationController. As it is stored inside a State field, but we usually want to iterate over its duration. Example:
class MyAnim extends StatefulWidget {
#override
_MyAnimState createState() => _MyAnimState();
}
class _MyAnimState extends State<MyAnim> with SingleTickerProviderStateMixin {
AnimationController animationController;
#override
void initState() {
animationController =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
super.initState();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
State provides a custom lifecycle hook for hot-reloads: reassemble
You can freely override that method to have custom hot-reload behaviors. Don't worry, this method will never be called in production.
With a small tweak you'd get the following:
class _MyAnimState extends State<MyAnim> with SingleTickerProviderStateMixin {
AnimationController animationController;
#override
void initState() {
animationController = AnimationController(vsync: this);
_initializeFields();
super.initState();
}
void _initializeFields() {
animationController.duration = const Duration(seconds: 1);
}
#override
void reassemble() {
_initializeFields();
super.reassemble();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
Now, whenever you modify your State class, it will correctly update AnimationController's duration.
Related
i send an idd from another screen when i want to navigate to this WaitingScreen
Now when i want to use the idd , I get an error in the line
FirebaseFirestore.instance.collection('ambulance').doc(idd).get()
class WaitingScreen extends StatefulWidget {
final String idd;
String? Key;
WaitingScreen({required this.idd}) : super();
_WaitingScreenState createState () => _WaitingScreenState( ) ;
}
class _WaitingScreenState extends State<WaitingScreen> with SingleTickerProviderStateMixin{
late AnimationController _controller;
#override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(minutes: 5));
_controller.forward();
}
#override
Widget build(BuildContext context) {
String? valuee;
return FutureBuilder<DocumentSnapshot>(
future: FirebaseFirestore.instance.collection('ambulance').doc(idd).get(),
When using a stateful widget, you need to reference variables on the widget itself by using the following construction:
widget.idd
By using the prefix widget., you make sure to access the variables on the widget, and not on the state you are in.
I made a widget 'ExpandableSection' which animates the child content by using a SizeTransition. It also works fine so far but I would like to have an additional parameter like "disableAtFirstBuild" so there is no initial animation but the widget is instantly shown. And only on rebuild the animation should be triggered. It seems like an easy task but I searched multiple hours for a solution without luck. For example I tried to set the animation duration to zero at first, invert a state boolean to save the fact that one build is done and afterwards set the duration to the normal value again. But somehow you cannot change an active controller. Is there a way to do it? Maybe it is quite easy and obvious but it is just not clear to me.
Any help would be much appreciated.
import 'package:flutter/widgets.dart';
class ExpandableSection extends StatefulWidget {
final Widget? child;
final bool? expand, useSliverScrollSafeMode;
final Axis axis;
ExpandableSection({this.expand = false, this.useSliverScrollSafeMode = false, this.axis = Axis.vertical, this.child});
#override
_ExpandableSectionState createState() => _ExpandableSectionState();
}
class _ExpandableSectionState extends State<ExpandableSection> with SingleTickerProviderStateMixin {
late AnimationController expandController;
late Animation<double> animation;
#override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
}
void prepareAnimations() {
expandController = AnimationController(vsync: this, duration: const Duration(milliseconds: 650));
animation = CurvedAnimation(
parent: expandController,
curve: Curves.easeInOutQuart,
);
}
void _runExpandCheck() {
if (widget.expand!) {
expandController.forward();
} else {
expandController.reverse();
}
}
#override
void didUpdateWidget(ExpandableSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runExpandCheck();
}
#override
void dispose() {
expandController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (widget.useSliverScrollSafeMode! && !expandController.isAnimating && !widget.expand!) {
return SizedBox.shrink();
} else {
return SizeTransition(axisAlignment: -1, axis: widget.axis, sizeFactor: animation, child: Center(child: widget.child));
}
}
}
Add 'value' to your controller while creating it; like this:
expandController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 650),
value: widget.expand ? 650 : 0,
);
Try stoping the animation controller in initState and then calling .forward() whenever you want it to run.
#override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
expandController.stop();
}
So a have a stateful parent with a timer value. This timer can be reset by the user. I also have a child stateful widget with an animation controller. The parent passes the timer value to this child to reset the animation. Here is some code:
class _ParentState extends State<Parent> {
int timer = 20;
void _userInput() {
setState(() {
timer = 20;
}
}
#override
Widget build(BuildContext context) {
return Child(
time: timer
);
}
}
Note: this is striped down for simplicity
So when my user triggers the _userInput method, the timer value gets changed but the child doesn't. Here is my full child widget:
class TimerProgress extends StatefulWidget {
TimerProgress({
Key key,
#required this.time,
#required this.onCompleted
}) : super(key: key);
final int time;
final Function onCompleted;
#override
_TimerProgressState createState() => _TimerProgressState();
}
class _TimerProgressState extends State<TimerProgress> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _widthAnimation;
#override
void initState() {
_controller = AnimationController(
duration: Duration(seconds: widget.time),
vsync: this
);
_widthAnimation = Tween<double>(
begin: 200,
end: 0
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.linear
));
_controller.addListener(() {
if (_controller.value == 1) {
widget.onCompleted();
}
});
_controller.forward();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return ...
}
}
So this all works, but ideally I want the animation to be reset, which doesn't work...
initState runs only once per state instance.
Implement resetting the time inside didUpdateWidget: https://api.flutter.dev/flutter/widgets/RawScrollbarState/didUpdateWidget.html
You can't (cause it will cause recursive loop, setState will trigger didUpdateWidget again) and shouldn't (after this method is invoked, build is being called) have to call setState inside this method.
Runnable example: https://www.dartpad.dev/848976603f866f4e3aa6030aba8addf7?null_safety=true
I do have a lot of code that looks like
this:
bool _somethingFromApiLoaded = false;
Something _somethingFromApi;
loadSomething() async {
final something = await ServiceProvider.of(context).apiService.getSomething();
setState(() => _somethingFromApi = something);
}
#override
Widget build(BuildContext context) {
if (!_somethingFromApiLoaded) {
loadSomething();
_somethingFromApiLoaded = true;
}
}
Note how I produce a lot of boilerplate code to ensure loadSomething is only called once.
I wonder if there isn't a lifecycle method to do so that I somehow misinterpret. I can't use initState because it does not have context.
I would try to a use a StatefulWidget and use initState() method.
That is the lifecycle you are referring to.
You should try to use a Future inside the initState()
#override
void initState() {
super.initState(); // make sure this is called in the beggining
// your code here runs only once
Future.delayed(Duration.zero,() {
_somethingFromApi = await ServiceProvider.of(context).apiService.getSomething();
});
}
As User gegobyte said, Context is available in the initState.
But apparently can't be used for everything.
You can use context in initState() by passing it to the widget:
class HomeScreen extends StatefulWidget {
final BuildContext context;
HomeScreen(this.context);
#override
State<StatefulWidget> createState() => new _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool _somethingFromApiLoaded = false;
Something _somethingFromApi;
loadSomething() async {
final something = await ServiceProvider.of(widget.context).apiService.getSomething();
setState(() => _somethingFromApi = something);
}
#override
void initState() {
super.initState();
if (!_somethingFromApiLoaded) {
loadSomething();
_somethingFromApiLoaded = true;
}
}
}
I have widget with data that changes regularly and I'm using a Timer.periodic to rebuild the widget. This starts out working smoothly but becomes choppy pretty quickly is there a better way to do this?
class _MainScreenState extends State<MainScreen> {
static const Duration duration = Duration(milliseconds: 16);
update(){
system.updatePos(duration.inMilliseconds/1000);
setState(() {});
}
#override
Widget build(BuildContext context) {
Timer.periodic(duration, (timer){
update();
});
return PositionField(
layoutSize: widget.square,
children: system.map
);
}
}
You are making a big mistake:
The build method must never have any side effects, because it is called again whenever setState is called (or when some higher up widget changes, or when the user rotates the screen...).
Instead, you want to create your Timer in initState, and cancel it on dispose:
class TimerTest extends StatefulWidget {
#override
_TimerTestState createState() => _TimerTestState();
}
class _TimerTestState extends State<TimerTest> {
Timer _timer;
int _foo = 0;
// this is only called once when the widget is attached
#override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (timer) => _update());
}
// stop the timer when the widget is detached and destroyed
#override
void dispose() {
_timer.cancel();
super.dispose();
}
void _update() {
setState(() {
_foo++;
});
}
#override
Widget build(BuildContext context) {
return Text('Foo: ${_foo}');
}
}