Flutter 框架跨平台鸿蒙开发 - 电台播放器应用开发教程
Flutter音频播放技术流媒体处理状态管理和数据持久化复杂UI布局设计用户交互优化这个应用展示了Flutter在多媒体应用开发中的强大能力,可以继续扩展更多功能,打造专业的电台播放平台!欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net。
·
Flutter电台播放器应用开发教程
项目简介
电台播放器是一款在线电台收听应用,支持多种电台分类浏览、收藏管理和播放历史记录。应用提供简洁的界面和流畅的播放体验,让用户随时随地收听喜爱的电台节目。
运行效果图



核心特性
- 多种分类:音乐、新闻、脱口秀、体育、文化、教育
- 在线播放:支持在线电台流媒体播放
- 收藏管理:收藏喜爱的电台
- 播放历史:记录播放历史
- 分类浏览:按分类筛选电台
- 搜索功能:快速查找电台
- 迷你播放器:底部常驻播放控制
- 数据持久化:本地保存收藏和历史
技术栈
- Flutter 3.x
- Material Design 3
- audioplayers(音频播放)
- SharedPreferences(数据持久化)
数据模型设计
电台分类枚举
enum RadioCategory {
music, // 音乐
news, // 新闻
talk, // 脱口秀
sports, // 体育
culture, // 文化
education, // 教育
}
电台模型
class RadioStation {
final String id; // 唯一标识
final String name; // 电台名称
final String description; // 描述
final String streamUrl; // 流媒体URL
final RadioCategory category; // 分类
final String? imageUrl; // 图标
final String? website; // 官网
bool isFavorite; // 是否收藏
}
播放历史模型
class PlayHistory {
final String stationId; // 电台ID
final DateTime playTime; // 播放时间
}
核心功能实现
1. 音频播放
使用audioplayers包实现电台播放:
final AudioPlayer _audioPlayer = AudioPlayer();
Future<void> _playStation(RadioStation station) async {
setState(() {
isLoading = true;
});
try {
if (currentStation?.id == station.id && isPlaying) {
await _audioPlayer.pause();
} else {
await _audioPlayer.stop();
await _audioPlayer.play(UrlSource(station.streamUrl));
setState(() {
currentStation = station;
isPlaying = true;
});
_addToHistory(station.id);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('播放失败: $e')),
);
} finally {
setState(() {
isLoading = false;
});
}
}
2. 播放状态监听
监听播放器状态变化:
void _setupAudioPlayer() {
_audioPlayer.onPlayerStateChanged.listen((state) {
setState(() {
isPlaying = state == PlayerState.playing;
isLoading = state == PlayerState.playing;
});
});
}
3. 收藏管理
void _toggleFavorite(String stationId) {
setState(() {
final station = stations.firstWhere((s) => s.id == stationId);
station.isFavorite = !station.isFavorite;
});
_saveData();
}
Future<void> _saveData() async {
final prefs = await SharedPreferences.getInstance();
// 保存收藏
final favoriteIds = stations
.where((s) => s.isFavorite)
.map((s) => s.id)
.toList();
await prefs.setStringList('favorite_stations', favoriteIds);
}
4. 播放历史
void _addToHistory(String stationId) {
final history = PlayHistory(
stationId: stationId,
playTime: DateTime.now(),
);
setState(() {
playHistory.insert(0, history);
if (playHistory.length > 50) {
playHistory.removeLast();
}
});
_saveData();
}
5. 数据持久化
Future<void> _loadData() async {
final prefs = await SharedPreferences.getInstance();
// 加载收藏状态
final favoritesData = prefs.getStringList('favorite_stations') ?? [];
final favoriteIds = favoritesData.toSet();
setState(() {
for (var station in stations) {
station.isFavorite = favoriteIds.contains(station.id);
}
});
// 加载播放历史
final historyData = prefs.getStringList('play_history') ?? [];
setState(() {
playHistory = historyData
.map((json) => PlayHistory.fromJson(jsonDecode(json)))
.toList();
});
}
UI组件设计
1. 迷你播放器
底部常驻的播放控制器:
Widget _buildMiniPlayer() {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: ListTile(
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(currentStation!.imageUrl ?? '📻'),
),
),
title: Text(currentStation!.name),
subtitle: Text(currentStation!.description),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow),
onPressed: () => _playStation(currentStation!),
),
IconButton(
icon: const Icon(Icons.stop),
onPressed: _stopPlaying,
),
],
),
),
);
}
2. 电台卡片
Widget _buildStationCard(RadioStation station) {
final isCurrentPlaying = currentStation?.id == station.id && isPlaying;
return Card(
child: ListTile(
leading: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: isCurrentPlaying
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Center(child: Text(station.imageUrl ?? '📻')),
),
title: Text(
station.name,
style: TextStyle(
fontWeight: isCurrentPlaying ? FontWeight.bold : FontWeight.normal,
),
),
subtitle: Text(station.description),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
station.isFavorite ? Icons.favorite : Icons.favorite_outline,
color: station.isFavorite ? Colors.red : null,
),
onPressed: () => _toggleFavorite(station.id),
),
IconButton(
icon: Icon(isCurrentPlaying ? Icons.pause : Icons.play_arrow),
onPressed: () => _playStation(station),
),
],
),
),
);
}
3. 推荐卡片
横向滚动的推荐电台:
Widget _buildRecommendCard(RadioStation station) {
return Card(
child: InkWell(
onTap: () => _playStation(station),
child: Container(
width: 150,
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(40),
),
child: Center(child: Text(station.imageUrl ?? '📻')),
),
const SizedBox(height: 12),
Text(
station.name,
style: const TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
);
}
4. 分类筛选
Widget _buildCategoryFilter() {
return SizedBox(
height: 60,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: RadioCategory.values.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return FilterChip(
label: const Text('全部'),
selected: selectedCategory == null,
onSelected: (selected) {
setState(() {
selectedCategory = null;
});
},
);
}
final category = RadioCategory.values[index - 1];
return FilterChip(
avatar: Icon(categoryIcons[category]),
label: Text(categoryNames[category]!),
selected: selectedCategory == category,
onSelected: (selected) {
setState(() {
selectedCategory = selected ? category : null;
});
},
);
},
),
);
}
应用架构
页面结构
数据流
功能扩展建议
1. 定时关闭
添加睡眠定时器:
class SleepTimer {
Timer? _timer;
int remainingMinutes = 0;
void start(int minutes, VoidCallback onComplete) {
remainingMinutes = minutes;
_timer = Timer.periodic(const Duration(minutes: 1), (timer) {
remainingMinutes--;
if (remainingMinutes <= 0) {
timer.cancel();
onComplete();
}
});
}
void cancel() {
_timer?.cancel();
remainingMinutes = 0;
}
}
// 使用
Future<void> _showSleepTimerDialog() async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('定时关闭'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('15分钟'),
onTap: () {
_sleepTimer.start(15, _stopPlaying);
Navigator.pop(context);
},
),
ListTile(
title: const Text('30分钟'),
onTap: () {
_sleepTimer.start(30, _stopPlaying);
Navigator.pop(context);
},
),
ListTile(
title: const Text('60分钟'),
onTap: () {
_sleepTimer.start(60, _stopPlaying);
Navigator.pop(context);
},
),
],
),
),
);
}
2. 录音功能
录制电台节目:
// 使用 record 包
import 'package:record/record.dart';
class RadioRecorder {
final Record _recorder = Record();
bool isRecording = false;
Future<void> startRecording(String stationName) async {
if (await _recorder.hasPermission()) {
final path = await _getRecordingPath(stationName);
await _recorder.start(
path: path,
encoder: AudioEncoder.aacLc,
);
isRecording = true;
}
}
Future<void> stopRecording() async {
final path = await _recorder.stop();
isRecording = false;
return path;
}
Future<String> _getRecordingPath(String stationName) async {
final directory = await getApplicationDocumentsDirectory();
final timestamp = DateTime.now().millisecondsSinceEpoch;
return '${directory.path}/${stationName}_$timestamp.aac';
}
}
3. 均衡器
添加音频均衡器:
class AudioEqualizer extends StatefulWidget {
final AudioPlayer player;
State<AudioEqualizer> createState() => _AudioEqualizerState();
}
class _AudioEqualizerState extends State<AudioEqualizer> {
Map<String, double> bands = {
'60Hz': 0,
'230Hz': 0,
'910Hz': 0,
'4kHz': 0,
'14kHz': 0,
};
Widget build(BuildContext context) {
return Column(
children: bands.entries.map((entry) {
return Column(
children: [
Text(entry.key),
Slider(
value: entry.value,
min: -12,
max: 12,
divisions: 24,
label: '${entry.value.toStringAsFixed(1)} dB',
onChanged: (value) {
setState(() {
bands[entry.key] = value;
});
_applyEqualizer();
},
),
],
);
}).toList(),
);
}
void _applyEqualizer() {
// 应用均衡器设置到播放器
}
}
4. 电台推荐
基于收听历史推荐电台:
class RadioRecommender {
List<RadioStation> getRecommendations(
List<RadioStation> allStations,
List<PlayHistory> history,
) {
// 统计各分类收听次数
final categoryCount = <RadioCategory, int>{};
for (var h in history) {
final station = allStations.firstWhere((s) => s.id == h.stationId);
categoryCount[station.category] =
(categoryCount[station.category] ?? 0) + 1;
}
// 找出最常听的分类
final topCategory = categoryCount.entries
.reduce((a, b) => a.value > b.value ? a : b)
.key;
// 推荐同分类的未收听电台
return allStations
.where((s) =>
s.category == topCategory &&
!history.any((h) => h.stationId == s.id)
)
.take(5)
.toList();
}
}
5. 电台节目表
显示电台节目时间表:
class ProgramSchedule {
final String time;
final String title;
final String description;
ProgramSchedule({
required this.time,
required this.title,
required this.description,
});
}
Widget _buildSchedulePage(RadioStation station) {
final schedule = [
ProgramSchedule(
time: '06:00 - 08:00',
title: '早间新闻',
description: '最新新闻资讯',
),
ProgramSchedule(
time: '08:00 - 10:00',
title: '音乐时光',
description: '轻松音乐陪伴',
),
// ... 更多节目
];
return ListView.builder(
itemCount: schedule.length,
itemBuilder: (context, index) {
final program = schedule[index];
return ListTile(
leading: const Icon(Icons.schedule),
title: Text(program.title),
subtitle: Text('${program.time}\n${program.description}'),
isThreeLine: true,
);
},
);
}
6. 社交分享
分享正在收听的电台:
// 使用 share_plus 包
import 'package:share_plus/share_plus.dart';
Future<void> _shareStation(RadioStation station) async {
final text = '''
我正在收听:${station.name}
${station.description}
${station.website ?? ''}
''';
await Share.share(text, subject: station.name);
}
7. 播客支持
添加播客节目支持:
class PodcastEpisode {
final String id;
final String title;
final String description;
final String audioUrl;
final Duration duration;
final DateTime publishDate;
PodcastEpisode({
required this.id,
required this.title,
required this.description,
required this.audioUrl,
required this.duration,
required this.publishDate,
});
}
class PodcastPlayer extends StatefulWidget {
final PodcastEpisode episode;
State<PodcastPlayer> createState() => _PodcastPlayerState();
}
class _PodcastPlayerState extends State<PodcastPlayer> {
final AudioPlayer _player = AudioPlayer();
Duration position = Duration.zero;
Duration duration = Duration.zero;
void initState() {
super.initState();
_player.onPositionChanged.listen((p) {
setState(() {
position = p;
});
});
_player.onDurationChanged.listen((d) {
setState(() {
duration = d;
});
});
}
Widget build(BuildContext context) {
return Column(
children: [
Slider(
value: position.inSeconds.toDouble(),
max: duration.inSeconds.toDouble(),
onChanged: (value) {
_player.seek(Duration(seconds: value.toInt()));
},
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.replay_10),
onPressed: () {
final newPosition = position - const Duration(seconds: 10);
_player.seek(newPosition);
},
),
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () => _player.play(UrlSource(widget.episode.audioUrl)),
),
IconButton(
icon: const Icon(Icons.forward_10),
onPressed: () {
final newPosition = position + const Duration(seconds: 10);
_player.seek(newPosition);
},
),
],
),
],
);
}
}
8. 离线下载
下载电台节目离线收听:
// 使用 dio 包
import 'package:dio/dio.dart';
class RadioDownloader {
final Dio _dio = Dio();
Future<void> downloadEpisode(
PodcastEpisode episode,
Function(double) onProgress,
) async {
final directory = await getApplicationDocumentsDirectory();
final savePath = '${directory.path}/${episode.id}.mp3';
await _dio.download(
episode.audioUrl,
savePath,
onReceiveProgress: (received, total) {
if (total != -1) {
onProgress(received / total);
}
},
);
}
Future<List<String>> getDownloadedEpisodes() async {
final directory = await getApplicationDocumentsDirectory();
final files = directory.listSync();
return files
.where((f) => f.path.endsWith('.mp3'))
.map((f) => f.path)
.toList();
}
}
9. 车载模式
简化界面适配车载使用:
class CarMode extends StatelessWidget {
final RadioStation? currentStation;
final bool isPlaying;
final VoidCallback onPlayPause;
final VoidCallback onNext;
final VoidCallback onPrevious;
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
currentStation?.name ?? '未播放',
style: const TextStyle(
color: Colors.white,
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 60),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.skip_previous, color: Colors.white),
iconSize: 80,
onPressed: onPrevious,
),
const SizedBox(width: 40),
IconButton(
icon: Icon(
isPlaying ? Icons.pause_circle : Icons.play_circle,
color: Colors.white,
),
iconSize: 120,
onPressed: onPlayPause,
),
const SizedBox(width: 40),
IconButton(
icon: const Icon(Icons.skip_next, color: Colors.white),
iconSize: 80,
onPressed: onNext,
),
],
),
],
),
),
);
}
}
10. 语音控制
添加语音命令控制:
// 使用 speech_to_text 包
import 'package:speech_to_text/speech_to_text.dart';
class VoiceController {
final SpeechToText _speech = SpeechToText();
Future<void> startListening(Function(String) onCommand) async {
bool available = await _speech.initialize();
if (available) {
_speech.listen(
onResult: (result) {
final command = result.recognizedWords.toLowerCase();
if (command.contains('播放')) {
onCommand('play');
} else if (command.contains('暂停')) {
onCommand('pause');
} else if (command.contains('停止')) {
onCommand('stop');
} else if (command.contains('下一个')) {
onCommand('next');
} else if (command.contains('上一个')) {
onCommand('previous');
}
},
);
}
}
void stopListening() {
_speech.stop();
}
}
性能优化建议
1. 音频缓冲
优化音频流缓冲:
await _audioPlayer.setReleaseMode(ReleaseMode.stop);
await _audioPlayer.setPlayerMode(PlayerMode.mediaPlayer);
// 设置缓冲大小
await _audioPlayer.setSourceUrl(
station.streamUrl,
isLocal: false,
);
2. 图片缓存
使用cached_network_image缓存电台图标:
// 使用 cached_network_image 包
import 'package:cached_network_image/cached_network_image.dart';
Widget _buildStationImage(String? imageUrl) {
if (imageUrl == null || imageUrl.startsWith('http')) {
return CachedNetworkImage(
imageUrl: imageUrl ?? '',
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.radio),
);
}
return Text(imageUrl); // Emoji
}
3. 列表优化
使用ListView.builder优化长列表:
ListView.builder(
itemCount: stations.length,
itemBuilder: (context, index) {
return _buildStationCard(stations[index]);
},
// 添加缓存范围
cacheExtent: 500,
)
测试建议
1. 单元测试
测试播放逻辑:
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Radio Player Tests', () {
test('Toggle favorite', () {
final station = RadioStation(
id: '1',
name: 'Test',
description: 'Test',
streamUrl: 'test',
category: RadioCategory.music,
isFavorite: false,
);
station.isFavorite = !station.isFavorite;
expect(station.isFavorite, true);
});
});
}
2. Widget测试
测试UI组件:
testWidgets('Station card displays correctly', (WidgetTester tester) async {
final station = RadioStation(
id: '1',
name: 'Test Station',
description: 'Test Description',
streamUrl: 'test',
category: RadioCategory.music,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StationCard(station: station),
),
),
);
expect(find.text('Test Station'), findsOneWidget);
expect(find.text('Test Description'), findsOneWidget);
});
部署发布
1. 依赖配置
在pubspec.yaml中添加:
dependencies:
audioplayers: ^6.0.0
shared_preferences: ^2.2.2
2. 权限配置
Android (android/app/src/main/AndroidManifest.xml):
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
iOS (ios/Runner/Info.plist):
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
3. 打包
# Android
flutter build apk --release
# iOS
flutter build ios --release
项目总结
通过开发这个电台播放器应用,你将掌握:
- Flutter音频播放技术
- 流媒体处理
- 状态管理和数据持久化
- 复杂UI布局设计
- 用户交互优化
这个应用展示了Flutter在多媒体应用开发中的强大能力,可以继续扩展更多功能,打造专业的电台播放平台!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐





所有评论(0)