I am learning Dart and making an app where you should hear an audio playback onTap
on a piece of text (Tile
). While playback is on, if another Tile
is tapped, I want the color of the previous unfinished recording to return to default, and the new playback tile to change.
This is my app at the moment:
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
List<String> titles = <String>[
'class1',
'class2',
'class3',
];
List<String> sentences = <String>[
'sentence1',
'sentence2',
'sentence3',
'sentence4'
];
List<String> audio = <String>[
'1.mp3',
'2.mp3',
'3.mp3',
'4.mp3'
];
void main() => runApp(const AppBarApp());
class AppBarApp extends StatelessWidget {
const AppBarApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
home: const AppBarExample(),
);
}
}
class AppBarExample extends StatefulWidget {
const AppBarExample({super.key});
@override
State<AppBarExample> createState() => _AppBarExampleState();
}
class _AppBarExampleState extends State<AppBarExample> {
@override
Widget build(BuildContext context) {
...//styling details ...
bool? isCurrentlyPlaying;
return DefaultTabController(
...//tabbar setup details ...
children: <Widget>[
ListView.builder(
itemCount: sentences.length,
itemBuilder:(BuildContext context, int index) {
return ListTile(
tileColor: index.isOdd
? oddItemColor : evenItemColor,
title: AudioPlayerWidget(index: index,
isCurrentlyPlaying: false,
onPlay: () {setState (() {isCurrentlyPlaying = true;});},
),
);
},
),
...//other items in the ListView ...
},
),
],
),
),
);
}
}
// AudioPlayer
final audioPlayer = AudioPlayer();
class AudioPlayerWidget extends StatefulWidget {
final int index;
final VoidCallback onPlay;
final bool isCurrentlyPlaying;
AudioPlayerWidget ({Key ? key, required this.index, required this.onPlay, required this.isCurrentlyPlaying}) : super(key: key);
@override
_AudioPlayerWidgetState createState() => _AudioPlayerWidgetState();
}
class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
bool isPlaying = false;
void _playAudio() {
audioPlayer.stop();
audioPlayer.play(AssetSource('audio/${widget.index+1}.mp3'));
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
widget.onPlay();
_playAudio();
},
child: Text(sentences[widget.index],
style: TextStyle(
color: widget.isCurrentlyPlaying ? Colors.indigo.shade900 : Colors.black,
)
),
);
}
}
The logic is as follows:
iscurrentlyPlaying
by default = false
widget.onPlay
isCurrentlyPlaying ? indigo : black
My issue is that onPlay() does not get triggered as expected, so the color remains black (default), and I am unable to diagnose why.
I previously successfully implemented alternative logic where colors change as expected if audio is allowed to play until the end. It is the interrupt logic, where I effectively want to have a value of "iscurrentlyplaying" assigned to each tile, that does not work.
After a day or so of hitting my head against the wall I found a way to achieve what I needed. In my original code I think I was confusing the state of AudioPlayer
widget and the ListView
widget. As a result, I was defining currentlyPlaying
twice, overwriting with false
each time.
Posting my solution here:
currentlyPlayingIndex
var to track which widget is playingfinal bool isCurrentlyPlaying;
onPlay
as well as onStop
callback functions
This allows for changing text color once playback is completed. It is also the function that is called in case of interrupting the original playback by tapping on another element in the UI. This way the state
which controls font color can be turned both on and off easily.onPlay: () {
setState(() {
isCurrentlyPlaying = true;
currentlyPlayingIndex = index;
});
},
onStop: () {
setState(() {
isCurrentlyPlaying = false;
currentlyPlayingIndex = null;
});
},
isCurrentlyPlaying
is only true
when the tapped widget is playing, and automatically false otherwiseisCurrentlyPlaying: isCurrentlyPlaying && currentlyPlayingIndex == index
_playAudio
function, use onPlay
rather than widget.onPlay
, and make use of onStop
as wellvoid _playAudio() {
audioPlayer.stop();
onPlay();
audioPlayer.play(AssetSource('audio/${index + 1}.mp3'));
audioPlayer.onPlayerComplete.listen((event) {
onStop();
});
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
List<String> titles = <String>[
'class1',
'class2',
'class3',
];
List<String> sentences = <String>[
'sentence1',
'sentence2',
'sentence3',
'sentence4'
];
List<String> audio = <String>[
'1.mp3',
'2.mp3',
'3.mp3',
'4.mp3'
];
void main() => runApp(const AppBarApp());
class AppBarApp extends StatelessWidget {
const AppBarApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
home: const AppBarExample(),
);
}
}
class AppBarExample extends StatefulWidget {
const AppBarExample({super.key});
@override
State<AppBarExample> createState() => _AppBarExampleState();
}
class _AppBarExampleState extends State<AppBarExample> {
// >> These vars control playback <<
bool isCurrentlyPlaying = false;
int? currentlyPlayingIndex;
@override
Widget build(BuildContext context) {
//...styling goes here
return DefaultTabController(
//... styling etc.
child: Scaffold(
appBar: AppBar(
title: const Text('My app'),
//...styling etc.
body: TabBarView(
children: <Widget>[
ListView.builder(
itemCount: sentences.length,
// >> Key widget playback logic <<
itemBuilder: (BuildContext context, int index) {
return ListTile(
tileColor: index.isOdd ? oddItemColor : evenItemColor,
title: AudioPlayerWidget(
index: index,
onPlay: () {
setState(() {
isCurrentlyPlaying = true;
currentlyPlayingIndex = index;
});
},
onStop: () {
setState(() {
isCurrentlyPlaying = false;
currentlyPlayingIndex = null;
});
},
isCurrentlyPlaying: isCurrentlyPlaying && currentlyPlayingIndex == index,
),
);
},
),
//...other ListView items (tiles)
// >> audioplayer definitions <<
final audioPlayer = AudioPlayer();
class AudioPlayerWidget extends StatelessWidget {
final int index;
final VoidCallback onPlay;
final VoidCallback onStop;
final bool isCurrentlyPlaying;
AudioPlayerWidget({Key? key, required this.index, required this.onPlay, required this.onStop, required this.isCurrentlyPlaying}) : super(key: key);
void _playAudio() {
// interrupt upon tap
audioPlayer.stop();
onPlay();
audioPlayer.play(AssetSource('audio/${index + 1}.mp3'));
// return to original color once playback finished
audioPlayer.onPlayerComplete.listen((event) {
onStop();
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
// >>conditionally kill the playback, and use `onStop` to update state and conditionally recolour font to default<<
if (isCurrentlyPlaying) {
audioPlayer.stop();
onStop();
} else {
_playAudio();
}
},
child: Text(
sentences[index],
style: >> font conditional coloring <<
color: isCurrentlyPlaying ? Colors.orange : Colors.blue,
),
),
);
}
}
I hope this helps somebody :)