前回から設定画面の作り込みと音楽再生機能の実装を進めています。
my_audio_player.dartクラスを途中まで作りました。これの続きを進めて音楽再生機能を完成させたいと思います。前回でコンストラクタ及びそこから呼ばれるメソッドを定義したと思います。今回はmain.dart側で更新されたら自動で呼ばれるメソッドを追加したいと思います。
my_audio_player.dart
class MyAudioPlayer {
static final _log = Logger('AudioController');
final AudioPlayer _musicPlayer;
final List<AudioPlayer> _sfxPlayers;
final Queue<Song> _playlist;
SettingsState? _settings;
ValueNotifier<AppLifecycleState>? _lifecycleNotifier;
int _currentSfxPlayer = 0;
MyAudioPlayer({int polyphony = 2})
: assert(polyphony >= 1),
_musicPlayer = AudioPlayer(playerId: 'musicPlayer'),
_sfxPlayers = Iterable.generate(
polyphony, (i) => AudioPlayer(playerId: 'sfxPlayer#$i'))
.toList(growable: false),
_playlist = Queue.of(List<Song>.of(songs)..shuffle()) {
_musicPlayer.onPlayerComplete.listen(_changeSong);
}
void _changeSong(void _) {
_log.info('Last song finished playing.');
_playlist.addLast(_playlist.removeFirst());
_playFirstSongInPlaylist();
}
Future<void> _playFirstSongInPlaylist() async {
_log.info(() => 'Playing ${_playlist.first} now.');
await _musicPlayer.play(AssetSource('music/${_playlist.first.filename}'));
}
void attachLifecycleNotifier(
ValueNotifier<AppLifecycleState> lifecycleNotifier) {
_lifecycleNotifier?.removeListener(_handleAppLifecycle);
lifecycleNotifier.addListener(_handleAppLifecycle);
_lifecycleNotifier = lifecycleNotifier;
}
void _handleAppLifecycle() {
switch (_lifecycleNotifier!.value) {
case AppLifecycleState.paused:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
_stopAllSound();
case AppLifecycleState.resumed:
if (_settings!.musicOn.value) {
_resumeMusic();
}
case AppLifecycleState.inactive:
break;
}
}
前回から以下の2つのメソッドを追加しました。
- attachLifecycleNotifier:ApplifecycleStateの状態に合わせて処理を行うコールバック関数を削除→追加する
- _handleAppLifecycle:ApplifecycleStateの状態に合わせて処理を行うコールバック関数
_handleAppLifecycleはAppLifecycleStateの状態によって音楽を再生・停止するようにしています。これによって、アプリを閉じたり終了したりした場合には音楽が停止し、アプリを開くと再生されるようになります。
_handleAppLifecycleで呼ばれている_stopAllSound()と_resumeMusic()も定義してしまいましょう。
void _stopAllSound() {
if (_musicPlayer.state == PlayerState.playing) {
_musicPlayer.pause();
}
for (final player in _sfxPlayers) {
player.stop();
}
}
Future<void> _resumeMusic() async {
_log.info('Resuming music');
switch (_musicPlayer.state) {
case PlayerState.paused:
try {
await _musicPlayer.resume();
} catch (e) {
_log.severe(e);
await _playFirstSongInPlaylist();
}
case PlayerState.stopped:
await _playFirstSongInPlaylist();
case PlayerState.playing:
case PlayerState.completed:
await _playFirstSongInPlaylist();
default:
_log.warning('Unhandled PlayerState: ${_musicPlayer.state}');
}
}
メソッド名の通りの処理ですね。audioPlayerの状態によって行う処理を変えています。
_resumeMusicだけちょっと長いですが、音楽再生の状態が一時停止状態の時は再開してそれ以外の時は最初から再生しています。
以上がアプリケーションの状態の変化によって呼ばれる設定です。
続いて音楽の再生・再生しないの設定を変更した際に呼ばれるメソッドを定義していきましょう。以下の2メソッドを追加します。
void attachSettings(SettingsState settingsState) {
if (_settings == settingsState) {
return;
}
final oldSettings = _settings;
if (oldSettings != null) {
oldSettings.musicOn.removeListener(_musicOnHandler);
}
_settings = settingsState;
settingsState.musicOn.addListener(_musicOnHandler);
if (settingsState.musicOn.value) {
_startMusic();
}
}
void _musicOnHandler() {
if (_settings!.musicOn.value) {
_resumeMusic();
} else {
_stopMusic();
}
}
構造はattachlifeCycleNotifierメソッドと似た感じですね。
attachSettingsでコールバック関数の入れ替えを行いつつ、引数に渡されたsettingsStateに現在の設定を置き換えています。
attachSettingsで呼ばれているコールバック関数の_musicOnHandlerで、settingsStateの音楽再生をするかどうかの値を読み取って音楽を再開するか・停止するかを呼び出しています。
_startMusic、_stopMusicも定義しましょう。
void _stopMusic() {
if (_musicPlayer.state == PlayerState.playing) {
_musicPlayer.pause();
}
}
void _startMusic() {
_playFirstSongInPlaylist();
}
これで状態が変更された際に呼ばれるメソッドを定義できました。
最後、終了時に呼ばれるdisposeメソッドを定義しましょう。以下のようになります。
void dispose() {
_lifecycleNotifier?.removeListener(_handleAppLifecycle);
_stopAllSound();
_musicPlayer.dispose();
for (final player in _sfxPlayers) {
player.dispose();
}
}
ValueNotifier<AppLifecycleState>のリスナを除去、再生を全てストップ、音楽再生を担当しているAudioPlayerのdispose(これの実装は忘れがち)を行っていますね。(まだ未実装ですが)効果音のプレイヤーの破棄もやっています。
音楽再生機能を試してみる
これで音楽再生の設定は終わったので早速再生されるか見てみたいのですが、main.dartの設定がまだでしたね。以下のようにmain.dartを変更しましょう。
@override
Widget build(BuildContext context) {
return AppLifecycleObserver(
child: MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) {
var counterState = CounterState(store: counterPersistence);
counterState.getFirstCounter();
return counterState;
}),
Provider<SettingsState>(
lazy: false,
create: (context) => SettingsState(
store: settingsPersistence,
)..loadSettingsFromPersistence(),
),
ProxyProvider2<SettingsState, ValueNotifier<AppLifecycleState>,
MyAudioPlayer>(
lazy: false,
create: (context) => MyAudioPlayer(),
update: (context, settings, lifecycleNotifier, audio) {
if (audio == null) throw ArgumentError.notNull();
audio.attachSettings(settings);
audio.attachLifecycleNotifier(lifecycleNotifier);
return audio;
},
dispose: (context, audio) => audio.dispose(),
),
],
child: Builder(builder: (context) {
return MaterialApp.router(
title: 'Flutter Demo',
theme: ThemeData.from(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
);
}),
),
);
}
}
上記のように、ProxyProvider2を新たに追加しています。
この内部でAudioPlayerのコンストラクタの定義、更新時のattachSettings,attachLifecycleNotifierの呼び出し、破棄時のdisposeメソッドの呼び出しを行っています。
ProxyProvider2の解説については以下の記事を参考にしましょう。
というわけで、アプリを起動して早速機能を試してみましょう。
※音が出るのでご注意ください!
このように、アプリの状態によって音楽の再生を止めたりなどの制御ができるようになりました。
まとめ
これで音楽再生機能をつけることができました。
一通りの機能を作成することができたので、この連載は一旦終わりにしたいと思います。
しばらくはFlutterの基礎知識的なところをメインで記事を書いていきたいと思います。
コメント