Related
I have an animatedContainer within a Widget, which includes a Container showing a number ("indicator"):
These indicators can increase/decrease or hide and show, but only exlusively. When negative, you see the red one, when positive, you see the green one. When 0, neither is shown.
The issue: The green one animates. The red one unintentionally does not. I have no idea why. It is basically the same widget, only with having empty spacing either on the right or left side, showing a positive or negative value and having either a green or red background color.
Here you see the class handling these "indicators". Keep in mind that in the image above you see two instances of the Widget.
import 'package:flutter/material.dart';
import 'package:progression_flutter/assets/colors.dart';
// TODO: Fix negative indicator unintended disabled animation.
class ProgressIndicators extends StatefulWidget {
const ProgressIndicators(this.valueStream,
{required this.showDigits, Key? key})
: super(key: key);
final Stream<num> valueStream;
final bool showDigits;
#override
State<ProgressIndicators> createState() => _ProgressIndicatorsState();
}
class _ProgressIndicatorsState extends State<ProgressIndicators> {
#override
Widget build(BuildContext context) {
return StreamBuilder<num>(
stream: widget.valueStream,
builder: (BuildContext context, AsyncSnapshot<num> snapshot) {
if (snapshot.hasData) {
final value = snapshot.data;
if (value != null) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 20),
_positionedIndicator(context, value: value),
],
);
}
}
return Container();
},
);
}
Widget _positionedIndicator(BuildContext context, {required num value}) {
final children = [
_animatedIndicator(context, value: value),
];
const spacer = SizedBox(
width: 60,
);
if (value.isNegative) {
children.add(spacer);
} else {
children.insert(0, spacer);
}
return Flexible(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: children,
),
);
}
Widget _animatedIndicator(BuildContext context, {required num value}) {
return AnimatedContainer(
transformAlignment: Alignment.topCenter,
curve: Curves.easeInOut,
height: value == 0 ? 0 : 20,
duration: const Duration(milliseconds: 250),
child: DownHangingProgressIndicator(
value: value,
showDigits: widget.showDigits,
),
);
}
}
class DownHangingProgressIndicator extends StatelessWidget {
const DownHangingProgressIndicator(
{Key? key, required this.value, required this.showDigits})
: super(key: key);
final num value;
final bool showDigits;
#override
Widget build(BuildContext context) {
final isNegative = value.isNegative;
final prefix = value.isNegative ? '' : '+';
final textValue = value.toStringAsFixed(showDigits ? 2 : 0);
return Stack(
alignment: Alignment.center,
children: [
Container(
height: 20,
width: 34,
decoration: BoxDecoration(
color: isNegative
? ProgressionColors.progressionRed
: ProgressionColors.progressionGreen,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(5),
bottomRight: Radius.circular(5),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
'$prefix$textValue',
style: const TextStyle(color: Colors.white, fontSize: 14),
textHeightBehavior:
const TextHeightBehavior(applyHeightToFirstAscent: false),
),
),
),
),
],
);
}
}
What causes the issue?
Running on iOS, flutter 3.3.4.
I used a flutter package called analog clock - link here to create a clock. I don't want to use Datetime.now but want to use a UTC time of +3. I've tweaked the UTC code a couple of times but still don't know to make it work with this package. There seems to be a bug with the package.
In my code I changed the date time to
DateTime.now().toUtc().add(Duration(hours: 3)), to show UTC +3.
However, whenever I run the app, the clock keeps showing the current daytime now. I've tried tweaking the code but cant fix
Here is my code where I call the analog clock package
class TimeAndWeatherPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context)
.size; //this gonna give us total height and with of our device
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Stack(
children: <Widget>[
Container(
// Here the height of the container is 45% of our total height
height: size.height * .50,
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(0, 50, 0, 10),
child: Center(
child: TitleText(
text: "time",
)),
),
CurrentDate(),
Center(
child: AnalogClock(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).shadowColor, width: 0.5),
color: Theme.of(context).cardColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
offset: Offset(0.0, 10.0),
blurRadius: 2.0,
spreadRadius: 0.0,
color: Theme.of(context).shadowColor,
),
],
),
width: 270.0,
height: 400,
isLive: true,
showTicks: true,
showAllNumbers: true,
showDigitalClock: true,
showNumbers: true,
showSecondHand: true,
hourHandColor: Theme.of(context).iconTheme.color,
minuteHandColor: Theme.of(context).iconTheme.color,
secondHandColor: Color(0xB35FD245),
numberColor: Theme.of(context).iconTheme.color,
textScaleFactor: 1.2,
digitalClockColor: Theme.of(context).iconTheme.color,
datetime:
DateTime.now().toUtc().add(Duration(hours: 3)),
),
),
],
),
),
)
],
),
);
}
}
here is the original code of the package
class AnalogClock extends StatefulWidget {
final DateTime datetime;
final bool showDigitalClock;
final bool showTicks;
final bool showNumbers;
final bool showAllNumbers;
final bool showSecondHand;
final Color hourHandColor;
final Color minuteHandColor;
final Color secondHandColor;
final Color tickColor;
final Color digitalClockColor;
final Color numberColor;
final bool isLive;
final double textScaleFactor;
final double width;
final double height;
final BoxDecoration decoration;
const AnalogClock(
{this.datetime,
this.showDigitalClock = true,
this.showTicks = true,
this.showNumbers = true,
this.showSecondHand = true,
this.showAllNumbers = false,
this.hourHandColor = Colors.black,
this.minuteHandColor = Colors.black,
this.secondHandColor = Colors.redAccent,
this.tickColor = Colors.grey,
this.digitalClockColor = Colors.black,
this.numberColor = Colors.black,
this.textScaleFactor = 1.0,
this.width = double.infinity,
this.height = double.infinity,
this.decoration = const BoxDecoration(),
isLive,
Key key})
: this.isLive = isLive ?? (datetime == null),
super(key: key);
const AnalogClock.dark(
{datetime,
showDigitalClock = true,
showTicks = true,
showNumbers = true,
showAllNumbers = false,
showSecondHand = true,
width = double.infinity,
height = double.infinity,
decoration = const BoxDecoration(),
Key key})
: this(
datetime: datetime,
showDigitalClock: showDigitalClock,
showTicks: showTicks,
showNumbers: showNumbers,
showAllNumbers: showAllNumbers,
showSecondHand: showSecondHand,
width: width,
height: height,
hourHandColor: Colors.white,
minuteHandColor: Colors.white,
secondHandColor: Colors.redAccent,
tickColor: Colors.grey,
digitalClockColor: Colors.white,
numberColor: Colors.white,
decoration: decoration,
key: key);
#override
_AnalogClockState createState() => _AnalogClockState(datetime);
}
class _AnalogClockState extends State<AnalogClock> {
DateTime datetime;
_AnalogClockState(datetime) : this.datetime = datetime ?? DateTime.now();
initState() {
super.initState();
if (widget.isLive) {
// update clock every second or minute based on second hand's visibility.
Duration updateDuration =
widget.showSecondHand ? Duration(seconds: 1) : Duration(minutes: 1);
Timer.periodic(updateDuration, update);
}
}
update(Timer timer) {
if (mounted) {
// update is only called on live clocks. So, it's safe to update datetime.
datetime = DateTime.now();
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
decoration: widget.decoration,
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: new Container(
constraints: BoxConstraints(minWidth: 48.0, minHeight: 48.0),
width: double.infinity,
child: new CustomPaint(
painter: new AnalogClockPainter(
datetime: datetime,
showDigitalClock: widget.showDigitalClock,
showTicks: widget.showTicks,
showNumbers: widget.showNumbers,
showAllNumbers: widget.showAllNumbers,
showSecondHand: widget.showSecondHand,
hourHandColor: widget.hourHandColor,
minuteHandColor: widget.minuteHandColor,
secondHandColor: widget.secondHandColor,
tickColor: widget.tickColor,
digitalClockColor: widget.digitalClockColor,
textScaleFactor: widget.textScaleFactor,
numberColor: widget.numberColor),
)))),
);
}
}
It looks like you might have a few things going on here.
You should be updating the datetime property within the setState call:
update(Timer timer) {
if (!mounted) return;
setState(() => datetime = DateTime.now());
}
You're updating the time without setting the timezone again, which would revert it to your local time. So the above should look like this:
update(Timer timer) {
if (!mounted) return;
setState(() => datetime = DateTime.now().toUtc());
// Or to an arbitrary time in the future, if that's what you need:
// setState(() => datetime = DateTime.now().toUtc().add(const Duration(hours: 3)));
}
Aside from that...
You should store a reference to the timer, so you can cancel() it in the widget's dispose() method.
Note that you are only updating the "mode" of the clock (seconds, or minutes) in the initState method of your widget, which only gets called once in the lifetime of a widget. It doesn't get called again when the widget is updated. For that you need to use didUpdateWidget.
OR...
I highly recommend you check out the Riverpod package, as it makes state management a lot easier. For example, the above code could look something like this...
final clockProvider = StateProvider<DateTime>((ref) => DateTime.now().toUtc());
class CustomClock extends ConsumerWidget{
...
#override
Widget build(BuildContext context, WidgetRef ref) {
return AnalogClock(
...
datetime: ref.watch(clockProvder),
...
);
}
}
class ClockModeButton extends ConsumerWidget{
...
#override
Widget build(BuildContext context, WidgetRef ref) {
return TextButton(
...
onPressed: () {
final DateTime newDateTime = DateTime.now().toUtc().add(const Duration(hours: 3));
ref.read(clockPrivder.notifier).state = newDateTime;
},
...
);
}
}
I have an app and I built some helper widgets to be able to reuse.
so I created this Button Widget which can show a loading icon if pressed and the async request is not completed. This is working fine when the app is hot restarted but when I am changing anything in the app or switching theme it is throwing an error that says this:
LateInitializationError: Field 'btnState' has not been initialized
However, it is working fine when creating this same concept using GetX package for state management for the busy and disabled state of the Button Widget but it is not working when using the flutter native way.
I think when switching theme GetX re-initialize all the widgets in the widget tree but StatefulWidget does not initialize it.
If you want to see GetX approach you can see here Flutter MVC Button Widget
Here is my Button Widget code
import 'package:flutter/material.dart';
import '../helpers/ColorPalette.dart';
import '../helpers/TextStyl.dart';
import 'LoadingIcon.dart';
class Button extends StatefulWidget {
late final _ButtonState btnState;
final String label;
final void Function(_ButtonState)? onTap;
final bool outline;
final Widget? leading;
final Widget? loadingIcon;
final bool block;
final Color? backgroundColor;
final Color? color;
final bool flat;
Button({
Key? key,
required this.label,
this.onTap,
this.leading,
this.loadingIcon,
this.flat = false,
this.backgroundColor = kcPrimary,
this.color,
}) : outline = false,
block = false,
super(key: key);
#override
State<Button> createState() {
btnState = _ButtonState();
return btnState;
}
}
class _ButtonState extends State<Button> {
bool isBusy = false;
bool isDisabled = false;
setBusy(bool val) {
setState(() {
isBusy = val;
});
return widget.btnState;
}
setDisabled(bool val) {
setState(() {
isDisabled = val;
});
return widget.btnState;
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (!isBusy && !isDisabled) {
widget.onTap!(widget.btnState);
}
},
child: widget.block
? AnimatedContainer(
duration: const Duration(milliseconds: 250),
padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 20.0),
width: double.infinity,
alignment: Alignment.center,
decoration: !widget.outline
? BoxDecoration(
color: !isDisabled ? widget.backgroundColor : widget.backgroundColor?.withOpacity(0.5),
borderRadius: BorderRadius.circular(!widget.flat ? 8 : 0),
)
: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(!widget.flat ? 8 : 0),
border: Border.all(
color: !isDisabled ? widget.backgroundColor! : widget.backgroundColor!.withOpacity(0.5),
width: 1,
),
),
child: !isBusy
? Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.leading != null) widget.leading!,
if (widget.leading != null) SizedBox(width: 5),
Text(
widget.label,
style: TextStyl.button(context)?.copyWith(
fontWeight: !widget.outline ? FontWeight.bold : FontWeight.w400,
color: !widget.outline
? widget.color != null
? widget.color
: getContrastColor(widget.backgroundColor!)
: widget.backgroundColor,
),
),
],
)
: widget.loadingIcon != null
? SizedBox(height: 20, width: 20, child: widget.loadingIcon)
: LoadingIcon(
color: !widget.outline
? widget.color != null
? widget.color
: getContrastColor(widget.backgroundColor!)
: widget.backgroundColor,
height: 16,
),
)
: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 250),
padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 20.0),
alignment: Alignment.center,
decoration: !widget.outline
? BoxDecoration(
color: !isDisabled ? widget.backgroundColor! : widget.backgroundColor!.withOpacity(0.5),
borderRadius: BorderRadius.circular(!widget.flat ? 8 : 0),
border: Border.all(
color: widget.backgroundColor!,
width: 1,
),
)
: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(!widget.flat ? 8 : 0),
border: Border.all(
color: widget.backgroundColor!,
width: 1,
),
),
child: !isBusy
? Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.leading != null) widget.leading!,
if (widget.leading != null) SizedBox(width: 5),
Text(
widget.label,
style: TextStyl.button(context)?.copyWith(
fontWeight: !widget.outline ? FontWeight.bold : FontWeight.w400,
color: !widget.outline
? widget.color != null
? widget.color
: getContrastColor(widget.backgroundColor!)
: widget.backgroundColor!,
),
),
],
)
: widget.loadingIcon != null
? SizedBox(height: 20, width: 20, child: widget.loadingIcon)
: LoadingIcon(
color: !widget.outline
? widget.color != null
? widget.color
: getContrastColor(widget.backgroundColor!)
: widget.backgroundColor!,
height: 16,
),
),
],
),
);
}
}
And here is the code where I used the Button Widget:
Button(
key: UniqueKey(),
label: "Test",
onTap: (btn) async {
btn.setBusy(true);
await Future.delayed(2.seconds);
btn.setBusy(false);
},
),
If you find yourself trying to manually access/manipulate the private _State class of any StatefulWidget then its a sign that you need to find a better approach to whatever it is you're trying to do.
I would avoid stuff like this
late final _ButtonState btnState; // this
...
#override
State<Button> createState() {
btnState = _ButtonState(); // this
return btnState;
}
...
final void Function(_ButtonState)? onTap; // using private state as arguments
If you want to do what that repo is doing with GetX in a native Flutter way then here's one way to approach it using ValueNotifier
If everything in the button depends on the status of an isBusy and isDisabled bool, then I'd create a custom class that ValueNotifier will sync to.
class ButtonStatus {
ButtonStatus({required this.isBusy, required this.isDisabled});
final bool isBusy;
final bool isDisabled;
}
Then a static controller class with relevant methods with similar functionality to the Getx class in the repo in addition to the same thing you're currently passing in as a test.
class ButtonStatusController {
// status is what ValueNotifier listens to
static ValueNotifier<ButtonStatus> status = ValueNotifier(
ButtonStatus(isBusy: false, isDisabled: false),
);
// equivalent to the function you're passing in the button
static Future<void> testFunction() async {
final isDisabled = status.value.isDisabled;
status.value = ButtonStatus(isBusy: true, isDisabled: isDisabled);
await Future.delayed(Duration(seconds: 2));
status.value = ButtonStatus(isBusy: false, isDisabled: isDisabled);
}
// updating ButtonStatus
static void updateButtonStatus(
{required bool isBusy, required bool isDisabled}) {
status.value = ButtonStatus(isBusy: isBusy, isDisabled: isDisabled);
}
}
Then your Button gets wrapped in a ValueListenableBuilder<ButtonStatus> and can be stateless. It will rebuild based on changes to the ButtonStatus and the functionality won't be affected by any changes to the Theme of the app.
class Button extends StatelessWidget {
final String label;
final void Function() onTap;
final bool outline;
final Widget? leading;
final Widget? loadingIcon;
final bool block;
final Color? backgroundColor;
final Color? color;
final bool flat;
Button({
Key? key,
required this.label,
required this.onTap,
this.leading,
this.loadingIcon,
this.flat = false,
this.backgroundColor = kcPrimary,
this.color,
}) : outline = false,
block = false,
super(key: key);
#override
Widget build(BuildContext context) {
// wrapping in ValueListenableBuilder of type ButtonStatus
return ValueListenableBuilder<ButtonStatus>(
valueListenable: ButtonStatusController
.status, // listening to any updates in the ButtonStatusController class
builder: (_, status, __) => GestureDetector(
// status here is what all the conditional builds in this widget now depend on
onTap: () {
if (!status.isBusy && !status.isDisabled) {
onTap();
}
},
child: block
? AnimatedContainer(
duration: const Duration(milliseconds: 250),
padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 20.0),
width: double.infinity,
alignment: Alignment.center,
decoration: !outline
? BoxDecoration(
color: !status.isDisabled
? backgroundColor
: backgroundColor?.withOpacity(0.5),
borderRadius: BorderRadius.circular(!flat ? 8 : 0),
)
: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(!flat ? 8 : 0),
border: Border.all(
color: !status.isDisabled
? backgroundColor!
: backgroundColor!.withOpacity(0.5),
width: 1,
),
),
child: !status.isBusy
? Row(
mainAxisSize: MainAxisSize.min,
children: [
if (leading != null) leading!,
if (leading != null) SizedBox(width: 5),
Text(
label,
style: TextStyl.button(context)?.copyWith(
fontWeight:
!outline ? FontWeight.bold : FontWeight.w400,
color: !outline
? color != null
? color
: getContrastColor(backgroundColor!)
: backgroundColor,
),
),
],
)
: loadingIcon != null
? SizedBox(height: 20, width: 20, child: loadingIcon)
: LoadingIcon(
color: !outline
? color != null
? color
: getContrastColor(backgroundColor!)
: backgroundColor,
height: 16,
),
)
: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 250),
padding:
EdgeInsets.symmetric(vertical: 12.0, horizontal: 20.0),
alignment: Alignment.center,
decoration: !outline
? BoxDecoration(
color: !status.isDisabled
? backgroundColor!
: backgroundColor!.withOpacity(0.5),
borderRadius: BorderRadius.circular(!flat ? 8 : 0),
border: Border.all(
color: backgroundColor!,
width: 1,
),
)
: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(!flat ? 8 : 0),
border: Border.all(
color: backgroundColor!,
width: 1,
),
),
child: !status.isBusy
? Row(
mainAxisSize: MainAxisSize.min,
children: [
if (leading != null) leading!,
if (leading != null) SizedBox(width: 5),
Text(
label,
style: TextStyl.button(context)?.copyWith(
fontWeight: !outline
? FontWeight.bold
: FontWeight.w400,
color: !outline
? color != null
? color
: getContrastColor(backgroundColor!)
: backgroundColor!,
),
),
],
)
: loadingIcon != null
? SizedBox(
height: 20, width: 20, child: loadingIcon)
: LoadingIcon(
color: !outline
? color != null
? color
: getContrastColor(backgroundColor!)
: backgroundColor!,
height: 16,
),
),
],
),
),
);
}
}
Besides that, I suggest reading up on refactoring widgets with large build methods because that one is a bit unruly. It could me made a lot easier to read by breaking it up into smaller StatelessWidgets.
I have a page where we have some pickup session when you select a pickup session at the bottom SwipeActionButton widget activate
now user can swipe right side and after swipe complete an async function execute which most time hit an api so if api result is success app goes to next page no problem here but if api result gave an error it shows a dialog
Press Ok and dialog pop but SwipeActionButton widget still show complete swipe how I can reset it.
code
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: NormalAppBar(
title: Text("Assign Requests"),
),
body: Consumer<PickupSessionProvider>(
builder: (context, provider, child) => Stack(
children: [
widget.requestIds.length == 0
? _requestsLoaded
? provider.unassignedRequestCount == 0
? Center(
child: Text("No requests.",
style: Theme.of(context).textTheme.headline6),
)
: _buildRequestsList(provider.unassignedRequests!)
: Center(
child: CircularProgressIndicator(),
)
: provider.pickupSessions!.length == 0
? Center(
child: Text("No active pickup session.",
style: Theme.of(context).textTheme.headline6),
)
: ListView(
padding: const EdgeInsets.only(bottom: 80),
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text(
"Select pickup session",
style: Theme.of(context).textTheme.headline4,
),
),
for (var pickupSession in provider.pickupSessions!)
_buildPickupSessionTile(pickupSession)
],
),
Positioned(
bottom: 0,
child: SwipeActionButton(
margin: EdgeInsets.symmetric(horizontal: 15, vertical: 20),
onDone: (_selectRequestList && requestIds.length == 0) ||
(!_selectRequestList &&
_selectedPickupSessionId == null)
? null
: () async {
var result = await showDialog(
context: context,
builder: (context) => _ProgressDialog(
requestIds: requestIds,
pickupSessionId: _selectedPickupSessionId),
barrierDismissible: true);
if (result == true) Navigator.of(context).pop(true);
},
doneText: "Assign request",
disabledText: "Assign request",
infoText: "Swipe to assign request",
),
)
],
),
),
);
}
Custom SwipeActionButton widget
class SwipeActionButton extends StatefulWidget {
final double height;
final Color doneColor;
final Color swiperColor;
final Color textColor;
final String doneText;
final String disabledText;
final VoidCallback? onDone;
final String? infoText;
final double? width;
final Color? backgroundColor;
final EdgeInsetsGeometry margin;
SwipeActionButton({
Key? key,
this.height = 50,
this.doneColor = const Color(0xff44b31f),
this.swiperColor = const Color(0xff44b31f),
this.textColor = const Color(0xff44b31f),
required this.doneText,
required this.disabledText,
this.onDone,
this.infoText,
this.width,
this.backgroundColor,
this.margin = EdgeInsets.zero
}) : super(key: key);
#override
_SwipeActionButtonState createState() => _SwipeActionButtonState();
}
class _SwipeActionButtonState extends State<SwipeActionButton>
with SingleTickerProviderStateMixin {
double swipePercent = 0.0;
bool swipeDone = false;
bool isDisabled = false;
late Color backgroundColor;
late AnimationController _controller;
Animation<double>? _animation;
void initState() {
super.initState();
backgroundColor = widget.backgroundColor ?? Color(0xff3344b31f);
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {
swipePercent = _animation?.value ??0;
});
})
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed && swipeDone) {
widget.onDone!();
}
});
}
void dispose() {
_controller.dispose();
super.dispose();
}
_onDragStart(DragStartDetails details) {
_controller.reset();
swipePercent = 0.0;
}
_onDragUpdate(DragUpdateDetails details) {
setState(() {
swipePercent =
details.globalPosition.dx / MediaQuery.of(context).size.width;
if (swipePercent > 0.90) swipeDone = true;
});
}
_onDragEnd(DragEndDetails details) {
if (swipePercent > 0.90 || swipeDone) {
_animation =
Tween<double>(begin: swipePercent, end: 1).animate(_controller);
} else {
_animation =
Tween<double>(end: 0, begin: swipePercent).animate(_controller);
}
_controller.forward();
}
#override
Widget build(BuildContext context) {
isDisabled = widget.onDone == null;
double screenWidth = MediaQuery.of(context).size.width;
return Container(
alignment: Alignment.center,
margin: widget.margin,
width: screenWidth - widget.margin.horizontal,
child: Stack(
clipBehavior: Clip.hardEdge,
children: <Widget>[
Container(
height: widget.height,
decoration: BoxDecoration(
color: isDisabled ? Colors.grey : Color(0xff3344b31f),
borderRadius: BorderRadius.all(Radius.circular(100.0)),
border: Border.all(
color: isDisabled ? Colors.grey : Color(0xff3344b31f),
width: 1.5,
),
),
child: Center(
child: Text(widget.infoText ?? "",
style: Theme.of(context)
.textTheme
.subtitle1!
.copyWith(color: widget.textColor))),
),
Container(
width: isDisabled
? screenWidth
: lerpDouble(widget.height, screenWidth, swipePercent),
height: widget.height,
child: Center(
child: Opacity(
opacity: isDisabled ? 1 : lerpDouble(0, 1, swipePercent)!,
child: Text(
isDisabled ? widget.disabledText : widget.doneText,
style: Theme.of(context).textTheme.subtitle1!.copyWith(
color: Colors.white,
),
textScaleFactor:
isDisabled ? 1 : lerpDouble(0, 1, swipePercent),
))),
decoration: BoxDecoration(
color: isDisabled ? Colors.grey : null,
borderRadius: BorderRadius.all(Radius.circular(100.0)),
/* border: Border.all(
color: Theme.of(context).primaryColor,
width: 1.5,
), */
gradient: isDisabled
? null
: LinearGradient(
begin: Alignment.center,
end: Alignment.centerRight,
colors: [
widget.doneColor,
swipeDone ? widget.doneColor : backgroundColor
])),
),
isDisabled
? Container()
: Positioned(
left: lerpDouble(
0, screenWidth -(15 +widget.margin.horizontal) - (widget.height * .9), swipePercent),
/* top: widget.height * .1,
bottom: widget.height * .1,
*/
child: AbsorbPointer(
absorbing: swipeDone,
child: GestureDetector(
onHorizontalDragStart: _onDragStart,
onHorizontalDragUpdate: _onDragUpdate,
onHorizontalDragEnd: _onDragEnd,
child: Opacity(
opacity: 1,
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
height: widget.height,
width: widget.height,
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(100.0)),
border: Border.all(
color: Theme.of(context).primaryColor,
width: 1.5,
),
boxShadow: swipeDone
? null
: [
BoxShadow(
color: Colors.black45,
blurRadius: 4)
],
color: swipeDone
? backgroundColor
: widget.swiperColor),
child: swipeDone
? Icon(
Icons.check,
size: 20,
color: Colors.white,
)
: Icon(
Icons.arrow_forward,
size: 20,
color: Colors.white,
),
))))),
],
),
);
}
}
You're asking how you can make the swipeButton reset in case the request doesn't return with a valid value.
The swipeButton's state is defined by its swipeDone and swipePercent variables. To achieve what you want you need to pass swipeDone as a parameter when constructing the widget.
class SwipeActionButton extends StatefulWidget {
// Make swipeDone a class variable for the widget
bool swipeDone;
final double height;
final Color doneColor;
final Color swiperColor;
final Color textColor;
final String doneText;
final String disabledText;
final VoidCallback? onDone;
final String? infoText;
final double? width;
final Color? backgroundColor;
final EdgeInsetsGeometry margin;
SwipeActionButton({
// Add it to the constructor
required this.swipeDone,
Key? key,
this.height = 50,
this.doneColor = const Color(0xff44b31f),
this.swiperColor = const Color(0xff44b31f),
this.textColor = const Color(0xff44b31f),
required this.doneText,
required this.disabledText,
this.onDone,
this.infoText,
this.width,
this.backgroundColor,
this.margin = EdgeInsets.zero,
}) : super(key: key);
#override
_SwipeActionButtonState createState() => _SwipeActionButtonState();
}
In _SwipeActionButtonState, delete bool swipeDone = false; and replace every swipeDone by widget.swipeDone.
You also need to reset the value of swipePercent.
You can do this by adding at the beginning of the swipeButton's build method :
if (widget.swipeDone == false && swipePercent > 0.9) swipePercent = 0;
Now you can declare the variable swipeDone in the parent widget state, pass it as a parameter and modify it whenever needed. For more clarity I give you an example with a simple widget that reset the swipe button when the floating action button is pressed.
Complete code :
import 'package:flutter/material.dart';
import 'dart:ui';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool swipeDone = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(),
body: SwipeActionButton(
disabledText: 'disabled',
doneText: 'doneText',
onDone: () {},
swipeDone: swipeDone,
),
floatingActionButton: FloatingActionButton(onPressed: () {
setState(() {
swipeDone = false;
});
}),
),
);
}
}
class SwipeActionButton extends StatefulWidget {
// Make swipeDone a class variable for the widget
bool swipeDone;
final double height;
final Color doneColor;
final Color swiperColor;
final Color textColor;
final String doneText;
final String disabledText;
final VoidCallback? onDone;
final String? infoText;
final double? width;
final Color? backgroundColor;
final EdgeInsetsGeometry margin;
SwipeActionButton({
// Add it to the constructor
required this.swipeDone,
Key? key,
this.height = 50,
this.doneColor = const Color(0xff44b31f),
this.swiperColor = const Color(0xff44b31f),
this.textColor = const Color(0xff44b31f),
required this.doneText,
required this.disabledText,
this.onDone,
this.infoText,
this.width,
this.backgroundColor,
this.margin = EdgeInsets.zero,
}) : super(key: key);
#override
_SwipeActionButtonState createState() => _SwipeActionButtonState();
}
class _SwipeActionButtonState extends State<SwipeActionButton> with SingleTickerProviderStateMixin {
double swipePercent = 0.0;
bool isDisabled = false;
late Color backgroundColor;
late AnimationController _controller;
Animation<double>? _animation;
#override
void initState() {
super.initState();
backgroundColor = widget.backgroundColor ?? Color(0xff3344b31f);
_controller = AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {
swipePercent = _animation?.value ?? 0;
});
})
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed && widget.swipeDone) {
widget.onDone!();
}
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
_onDragStart(DragStartDetails details) {
_controller.reset();
swipePercent = 0.0;
}
_onDragUpdate(DragUpdateDetails details) {
setState(() {
swipePercent = details.globalPosition.dx / MediaQuery.of(context).size.width;
if (swipePercent > 0.90) widget.swipeDone = true;
});
}
_onDragEnd(DragEndDetails details) {
if (swipePercent > 0.90 || widget.swipeDone) {
_animation = Tween<double>(begin: swipePercent, end: 1).animate(_controller);
} else {
_animation = Tween<double>(end: 0, begin: swipePercent).animate(_controller);
}
_controller.forward();
}
#override
Widget build(BuildContext context) {
if (widget.swipeDone == false && swipePercent > 0.9) swipePercent = 0;
isDisabled = widget.onDone == null;
double screenWidth = MediaQuery.of(context).size.width;
return Container(
alignment: Alignment.center,
margin: widget.margin,
width: screenWidth - widget.margin.horizontal,
child: Stack(
clipBehavior: Clip.hardEdge,
children: <Widget>[
Container(
height: widget.height,
decoration: BoxDecoration(
color: isDisabled ? Colors.grey : Color(0xff3344b31f),
borderRadius: BorderRadius.all(Radius.circular(100.0)),
border: Border.all(
color: isDisabled ? Colors.grey : Color(0xff3344b31f),
width: 1.5,
),
),
child: Center(
child: Text(widget.infoText ?? "",
style: Theme.of(context).textTheme.subtitle1!.copyWith(color: widget.textColor))),
),
Container(
width: isDisabled ? screenWidth : lerpDouble(widget.height, screenWidth, swipePercent),
height: widget.height,
child: Center(
child: Opacity(
opacity: isDisabled ? 1 : lerpDouble(0, 1, swipePercent)!,
child: Text(
isDisabled ? widget.disabledText : widget.doneText,
style: Theme.of(context).textTheme.subtitle1!.copyWith(
color: Colors.white,
),
textScaleFactor: isDisabled ? 1 : lerpDouble(0, 1, swipePercent),
))),
decoration: BoxDecoration(
color: isDisabled ? Colors.grey : null,
borderRadius: BorderRadius.all(Radius.circular(100.0)),
/* border: Border.all(
color: Theme.of(context).primaryColor,
width: 1.5,
), */
gradient: isDisabled
? null
: LinearGradient(
begin: Alignment.center,
end: Alignment.centerRight,
colors: [widget.doneColor, widget.swipeDone ? widget.doneColor : backgroundColor])),
),
isDisabled
? Container()
: Positioned(
left:
lerpDouble(0, screenWidth - (15 + widget.margin.horizontal) - (widget.height * .9), swipePercent),
/* top: widget.height * .1,
bottom: widget.height * .1,
*/
child: AbsorbPointer(
absorbing: widget.swipeDone,
child: GestureDetector(
onHorizontalDragStart: _onDragStart,
onHorizontalDragUpdate: _onDragUpdate,
onHorizontalDragEnd: _onDragEnd,
child: Opacity(
opacity: 1,
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
height: widget.height,
width: widget.height,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(100.0)),
border: Border.all(
color: Theme.of(context).primaryColor,
width: 1.5,
),
boxShadow:
widget.swipeDone ? null : [BoxShadow(color: Colors.black45, blurRadius: 4)],
color: widget.swipeDone ? backgroundColor : widget.swiperColor),
child: widget.swipeDone
? Icon(
Icons.check,
size: 20,
color: Colors.white,
)
: Icon(
Icons.arrow_forward,
size: 20,
color: Colors.white,
),
))))),
],
),
);
}
}
I have a chat bubble widget which has a audio player support with a Slider widget.
The slider's value is changed according to the AudioPlayer's progress which seems to work fine.
When the first audio is played completely (meaning the slider's value is now 100%), & now the second chat bubble is added to the AnimatedList then the newest Slider has the value of 100 & the previous has the value of 0.
Here's an example to understand better:
Message 1 added to list: Audio Played completed => Slider value is 100.
Message 2 added to list: Slider value is 100 (should be 0) & the slider from message 1 has value of 0.
Here's the widget:
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
class MessageBubbleAudioPlayer extends StatefulWidget {
final Color color;
final String audioUrl;
const MessageBubbleAudioPlayer({
#required this.audioUrl,
#required this.color,
});
#override
_MessageBubbleAudioPlayerState createState() =>
_MessageBubbleAudioPlayerState();
}
class _MessageBubbleAudioPlayerState extends State<MessageBubbleAudioPlayer> {
bool loading = false;
bool isPlaying = false;
double audioSeekValue = 0;
final AudioPlayer audioPlayer = AudioPlayer();
Duration totalDuration = Duration(milliseconds: 0);
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
audioPlayer.onPlayerStateChanged.listen((event) {
if (mounted) setState(() => isPlaying = event == PlayerState.PLAYING);
});
audioPlayer.onAudioPositionChanged.listen((event) {
final percent =
((event.inMilliseconds * 100) / totalDuration.inMilliseconds) ?? 0;
if (mounted) setState(() => audioSeekValue = percent);
});
});
}
#override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
loading
? Container(
height: 30,
width: 30,
padding: const EdgeInsets.all(8),
child: CircularProgressIndicator(
color: widget.color,
strokeWidth: 1.8,
),
)
: Container(
width: 30,
child: IconButton(
icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow,
color: widget.color),
onPressed: () async {
if (audioPlayer.state == PlayerState.PAUSED) {
audioPlayer.resume();
return;
}
if (!isPlaying) {
setState(() => loading = true);
await audioPlayer.play(widget.audioUrl);
audioPlayer.getDuration().then((value) {
totalDuration = Duration(milliseconds: value);
setState(() => loading = false);
});
} else
await audioPlayer.pause();
},
splashRadius: 25,
),
),
SliderTheme(
data: SliderThemeData(
trackHeight: 1.4,
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7)),
child: Slider(
label: "Audio",
activeColor: widget.color,
inactiveColor: widget.color.withAlpha(100),
// this (value) should be 0 for a newly added widget
// but is 100 for the newer one & 0 for the previous one,
// which infact should be opposite
value: audioSeekValue,
min: 0,
max: 100,
onChanged: (_) {},
),
)
],
);
}
}
This widget is in turn used in another widget which handles the type of message & shows appropriate ui.
Here it is:
class MessageBubble extends StatelessWidget {
final bool isSender, isAudio;
final String message;
const MessageBubble(this.message, this.isSender, this.isAudio, Key key)
: super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 3),
child: Align(
alignment: isSender ? Alignment.centerRight : Alignment.centerLeft,
child: message.contains(Constants.emojiRegex, 0) &&
!message.contains(Constants.alphaNumericRegex)
? Padding(
padding: EdgeInsets.only(
top: 6,
bottom: 6,
left: isSender ? 16 : 0,
right: isSender ? 0 : 32),
child: Text(message,
style: TextStyle(fontSize: 45, color: Colors.white)),
)
: Material(
borderRadius: BorderRadius.circular(30),
elevation: 4,
color: isSender
? Colors.deepPurpleAccent.shade100.darken()
: Colors.white,
child: isAudio
? Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 6),
child: MessageBubbleAudioPlayer(
key: ValueKey(message.hashCode.toString()),
audioUrl: message,
color: isSender
? Colors.white
: Colors.deepPurpleAccent,
),
)
: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 14),
child: Linkify(
onOpen: (link) async {
if ((await canLaunch(link.url)))
await launch(link.url);
},
options: LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: isSender
? Colors.white
: Colors.deepPurpleAccent),
text: message,
style: TextStyle(
fontSize: 17,
color: isSender ? Colors.white : Colors.black),
),
),
)),
);
}
}
And here's the AnimatedList:
class ChatAnimatedList extends StatefulWidget {
final bool isInfoShown, isSender;
const ChatAnimatedList(
{#required Key key, #required this.isInfoShown, this.isSender})
: super(key: key);
#override
ChatAnimatedListState createState() => ChatAnimatedListState();
}
class ChatAnimatedListState extends State<ChatAnimatedList> {
final _messageList = <MessageBubble>[];
final _animatedListState = GlobalKey<AnimatedListState>();
get messageLength => _messageList.length;
insertMessageBubble(MessageBubble messageBubble) =>
_messageList.insert(0, messageBubble);
insertViaState() {
if (_animatedListState.currentState != null)
_animatedListState.currentState.insertItem(0);
}
#override
Widget build(BuildContext context) {
return widget.isInfoShown
? InfoPlaceholder(isSender: widget.isSender)
: Expanded(
child: AnimatedList(
reverse: true,
key: _animatedListState,
initialItemCount: _messageList.length,
itemBuilder: (_, index, animation) {
return index == 0
? Padding(
padding: const EdgeInsets.only(bottom: 6),
child: _messageList[index])
: index == _messageList.length - 1
? Padding(
padding: const EdgeInsets.only(top: 30),
child: _messageList[index])
: _messageList[index];
}),
);
}
}
I've also tried using AutomaticKeepAliveClientMixin but still no use.
Any thoughts on this would be appreciated.
This probably happening due to your Widgets being of the same type.
While flutter checks the changes in the widget tree, it checks for the type of the widget as well the key provided while creating that widget.
From you example, it is clear that no key is being provided while creating the StatefulWidget.
So, when you are pushing a new Widget (and I assume you are pushing this widget earlier than the old widget in the tree), flutter thinks this is still the older widget and assigns the older State object to it.
Start, sending a unique key whenever you are creating new StatefulWidgets that exist inside a List type widgets like Row, Column etc.,
class MessageBubbleAudioPlayer extends StatefulWidget {
const MessageBubbleAudioPlayer({
#required this.audioUrl,
#required this.color,
Key key.
}) : super(key: key);
While creating a new one,
MessageBubbleAudioPlayer(audioUrl: '', color: '', key: ValueKey(#some unique int or string#)
In place of #some unique int or string# put something that is going to be unique to that widget, not an index since that can change, but you can use the audioUrl itself as a key.
When your audio play is completed make audioSeekValue = 0 . This will start from the begening.
If you want to keep track :
Song 1 played = 70%
Song 2 played = 50%
In this case, you have to either keep your index song played valued in a list or get the song played value dynamically from the backend.
Please let me know if it helps.