Slider in AnimatedList has wrong value

331 views Asked by At

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.

2

There are 2 answers

0
shorol On
  1. When your audio play is completed make audioSeekValue = 0 . This will start from the begening.

  2. 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.

9
Nisanth Reddy On

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.