简体   繁体   中英

How to maintain state of widget in animated list

I have an app where I have a list of timers. The Timer() widget is very simple. It's an animated builder that has a Column() with a Container() containing the pause/play button, name, time left, delete, and repeat. All of this information is stored in a TimerData() class.

The problem is that when I delete a timer above a running timer, it gets reset: https://cln.sh/VoHxCL .

I display the timers using an AnimatedList() . Here is my code:

Timer() :

class Timer extends StatefulWidget {
  Duration time;
  String name;
  void Function() onDelete;
  Timer(
      {Key? key,
      required this.time,
      required this.name,
      required this.onDelete})
      : super(key: key);

  @override
  TimerState createState() => TimerState();
}

class TimerState extends State<Timer> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: widget.time);
  }

  void pause() => _controller.stop();
  void play() async {
    if (_controller.value == 1) {
      await _controller.animateTo(0,
          duration: const Duration(milliseconds: 100), curve: Curves.linear);
    }
    _controller.forward();
  }

  void toggle() => _controller.status == AnimationStatus.forward
      ? _controller.stop()
      : _controller.forward();
  void reset() => _controller.animateTo(0,
      duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);

  void _onFinish() {
    // TODO play a sound and give a notification
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: _controller,
        builder: (context, _) {
          return Container(
            margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
            decoration: BoxDecoration(
              color: Theme.of(context).cardColor,
              borderRadius: BorderRadius.circular(10),
            ),
            clipBehavior: Clip.hardEdge,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                Padding(
                  padding: const EdgeInsets.only(left: 5, right: 5, top: 5),
                  child: Row(
                    mainAxisSize: MainAxisSize.max,
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Row(
                        children: [
                          // Play/Pause
                          CupertinoButton(
                            child: _controller.isAnimating
                                ? const Icon(CupertinoIcons.pause_solid)
                                : const Icon(CupertinoIcons.play_arrow_solid),
                            onPressed: () => setState(() {
                              if (_controller.isAnimating) {
                                pause();
                              } else {
                                play();
                              }
                            }),
                          ),
                          const SizedBox(width: 10),
                          // Time
                          Text(
                              '${widget.name} – ${_formatDuration(_controller) ?? 'Error formatting date'}'),
                          // const Text('TIME'),
                        ],
                      ),
                      Row(
                        children: [
                          // Reset
                          CupertinoButton(
                            child: const Icon(CupertinoIcons.restart),
                            onPressed: () => setState(() {
                              pause();
                              reset();
                            }),
                          ),
                          // Delete
                          CupertinoButton(
                            child: const Icon(CupertinoIcons.delete_solid),
                            onPressed: widget.onDelete,
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
                // Progress Bar
                Align(
                  alignment: Alignment.centerLeft,
                  child: Container(
                    color: Theme.of(context).colorScheme.primary,
                    height: 5,
                    width: (MediaQuery.of(context).size.width - (15 * 2)) *
                        _controller.value,
                  ),
                ),
              ],
            ),
          );
        });
  }
}

Home() widget containing the AnimatedList() :

class Home extends StatefulWidget {
  const Home({Key? key}) : super(key: key);

  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
  List<TimerData> timers = [];

  @override
  Widget build(BuildContext context) {
    AppData data = Provider.of<AppData>(context);

    return Scaffold(
      extendBodyBehindAppBar: timers.isEmpty,
      backgroundColor: Theme.of(context).brightness == Brightness.dark
          ? null
          : Color.fromRGBO(
              Theme.of(context).scaffoldBackgroundColor.red - 15,
              Theme.of(context).scaffoldBackgroundColor.green - 15,
              Theme.of(context).scaffoldBackgroundColor.blue - 15,
              1),
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        actions: [
          CupertinoButton(
            child: const Icon(CupertinoIcons.add),
            onPressed: () => showDialog(
                context: context,
                builder: (context) {
                  String? name;
                  Duration days = const Duration(days: 0);
                  Duration hours = const Duration(hours: 0);
                  Duration minutes = const Duration(minutes: 0);
                  Duration seconds = const Duration(seconds: 0);

                  return AlertDialog(
                    shape: const RoundedRectangleBorder(
                      borderRadius: BorderRadius.all(
                        Radius.circular(10),
                      ),
                    ),
                    content: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            const Text('Name:'),
                            const SizedBox(width: 10),
                            SizedBox(
                              height: 35,
                              width: 140,
                              child: CupertinoTextField(
                                onChanged: (val) => name = val,
                                placeholder: 'Timer',
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 20),
                        Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            const Text('Days:'),
                            const SizedBox(width: 10),
                            SizedBox(
                              height: 30,
                              width: 50,
                              child: CupertinoTextField(
                                placeholder: '0',
                                keyboardType: TextInputType.number,
                                inputFormatters: <TextInputFormatter>[
                                  FilteringTextInputFormatter.digitsOnly
                                ], // Only numbers can be entered
                                onChanged: (val) =>
                                    days = Duration(days: int.parse(val).abs()),
                              ),
                            ),
                            const SizedBox(width: 5),
                            const Text('Hours:'),
                            const SizedBox(width: 10),
                            SizedBox(
                              height: 30,
                              width: 50,
                              child: CupertinoTextField(
                                placeholder: '0',
                                keyboardType: TextInputType.number,
                                inputFormatters: <TextInputFormatter>[
                                  FilteringTextInputFormatter.digitsOnly
                                ], // Only numbers can be entered
                                onChanged: (val) => hours =
                                    Duration(hours: int.parse(val).abs()),
                              ),
                            ),
                            const SizedBox(width: 5),
                            const Text('Minutes:'),
                            const SizedBox(width: 10),
                            SizedBox(
                              height: 30,
                              width: 50,
                              child: CupertinoTextField(
                                placeholder: '0',
                                keyboardType: TextInputType.number,
                                inputFormatters: <TextInputFormatter>[
                                  FilteringTextInputFormatter.digitsOnly
                                ], // Only numbers can be entered
                                onChanged: (val) => minutes =
                                    Duration(minutes: int.parse(val).abs()),
                              ),
                            ),
                            const SizedBox(width: 5),
                            const Text('Seconds:'),
                            const SizedBox(width: 10),
                            SizedBox(
                              height: 30,
                              width: 50,
                              child: CupertinoTextField(
                                placeholder: '0',
                                keyboardType: TextInputType.number,
                                inputFormatters: <TextInputFormatter>[
                                  FilteringTextInputFormatter.digitsOnly
                                ], // Only numbers can be entered
                                onChanged: (val) => seconds =
                                    Duration(seconds: int.parse(val).abs()),
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 20),
                        CupertinoButton.filled(
                          child: const Text('Add Timer'),
                          onPressed: () {
                            Duration time = days + hours + minutes + seconds;

                            if (time.inSeconds == 0) {
                              time = const Duration(minutes: 1);
                            }

                            setState(() {
                              timers.add(TimerData(
                                  id: const Uuid().v1(),
                                  name: name ?? 'Timer',
                                  time: time));

                              if (_listKey.currentState != null) {
                                _listKey.currentState!.insertItem(
                                  timers.length - 1,
                                  duration: const Duration(milliseconds: 300),
                                );
                              }
                            });

                            Navigator.pop(context);
                          },
                        ),
                      ],
                    ),
                  );
                }),
          ),
          CupertinoButton(
            child: Icon(data.themeMode == ThemeMode.light
                ? CupertinoIcons.lightbulb_fill
                : (data.themeMode == ThemeMode.dark
                    ? CupertinoIcons.moon_stars_fill
                    : CupertinoIcons.gear_solid)),
            onPressed: () {
              // TODO change theme

              // ! temp
              if (data.themeMode == ThemeMode.system) {
                data.setTheme(ThemeMode.light);
              } else if (data.themeMode == ThemeMode.light) {
                data.setTheme(ThemeMode.dark);
              } else {
                data.setTheme(ThemeMode.system);
              }
            },
          ),
        ],
      ),
      body: timers.isNotEmpty
          ? AnimatedList(
              key: _listKey,
              initialItemCount: timers.length,
              itemBuilder: (context, index, animation) {
                return SizeTransition(
                  sizeFactor: CurvedAnimation(
                    parent: animation,
                    curve: Curves.easeInOut,
                  ),
                  child: Timer(
                    key: ValueKey(timers[index].id),
                    name: timers[index].name,
                    time: timers[index].time,
                    onDelete: () {
                      ValueKey key = ValueKey(timers[index].id);
                      String name = timers[index].name;
                      Duration time = timers[index].time;

                      setState(() {
                        timers.removeAt(index);

                        _listKey.currentState!.removeItem(
                          index,
                          (context, animation) {
                            return SizeTransition(
                              sizeFactor: CurvedAnimation(
                                parent: animation,
                                curve: Curves.easeInOut,
                              ),
                              child: Timer(
                                key: key,
                                name: name,
                                time: time,
                                onDelete: () {},
                              ),
                            );
                          },
                          duration: const Duration(milliseconds: 300),
                        );
                      });
                    },
                  ),
                );
              },
            )
          : const Center(child: Text('No Timers')),
    );
  }
}

I was able to fix this by usins GlobalKey s.

I made the TimerState public by removing the _ and made a list of GlobalKey<TimerState> s and then added it to the Timer() widget when building it in the aniamted list.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM