FlutterのCasual Game ToolKitを使ってみる! その6

ブログ
samples/game_template at main · flutter/samples
AcollectionofFlutterexamplesanddemos.Contributetoflutter/samplesdevelopmentbycreatinganaccountonGitHub.

前回から引き続き、サンプルコードを参考にして設定画面及び設定画面の変数を設定していきます。

前回、設定画面の設定はProviderパッケージのChangeNotifierを使用していたと思います。ですが、これをProviderのValueNotifierを使用すると、もう少しシンプルに書けたりクロージャーの呼び出し等を行うことができるようです。

早速設定画面で変更するパラメータを管理しているsettings_state.dartをChangeNotifierではなくValueNotifierを使用して書き換えてみましょう。

settings_state.dart

class SettingsState extends ChangeNotifier {
  final SettingsPersistence _store;
  bool _musicOn = false;

  SettingsState({required SettingsPersistence store}) : _store = store;

  bool getMusicOn() {
    return _musicOn;
  }

  Future<void> loadSettingsFromPersistence() async {
    await Future.wait([_store.getMusicOn().then((value) => _musicOn = value)]);
  }

  void toggleMusicOn() {
    _musicOn = !_musicOn;
    notifyListeners();
    _store.setMusicOn(_musicOn);
  }
}

これが前回までの設定画面のパラメータのクラスです。現状変数としては_musicOn(音楽を再生するか?)の1つだけですね。これをValueNotifierを使用するように書き換えます。

class SettingsState {
  final SettingsPersistence _store;
  ValueNotifier<bool> musicOn = ValueNotifier(false); <- 変更

  SettingsState({required SettingsPersistence store}) : _store = store;

  Future<void> loadSettingsFromPersistence() async {
    await Future.wait(
        [_store.getMusicOn().then((value) => musicOn.value = value)]); <- 変更
  }

  void toggleMusicOn() {
    musicOn.value = !musicOn.value; <- 変更
    _store.setMusicOn(musicOn.value); <- 変更
  }
}

上記のように変更しました。

ValueNotifierを使うメリットとその使い方 - Qiita
「とりあえずChangeNotifier」は本当に最適解?「とりあえずChangeNotifierでできるからChangeNotifierで」という理由でChangeNotifierを使っていませ…

上にも記載がありますが、ValueNotifierを使用するとchangeNotifier()を呼び出さずとも値の変更を検知してValueNotifier側でchangeNotifier()を呼んでくれるため、宣言し忘れを防ぐことができます。

では、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(), <- 変更
          ),
        ],
...(省略)

こんな感じですね。ただ、これだけだとValueNotifierが管理している変数の変更をUIに知らせてウィジェットツリーの再ビルドを依頼することができません。これをUIに知らせるには変化するUIの部分にValueListenableBuidlerを使用します。

というわけで、ValueNotifierを使用しているsettings_page.dartを変更します。

settings_page.dart

class SettingsPage extends StatelessWidget {
  const SettingsPage({super.key, required this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    final settings = context.watch<SettingsState>();
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(title),
      ),
      body: ResponsiveScreen(
          squarishMainArea: ListView(
            children: [
              const SizedBox(height: 60),
              const Text('Settings',
                  textAlign: TextAlign.center, style: TextStyle(fontSize: 40)),
              const SizedBox(height: 40),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Text('Music : ', style: TextStyle(fontSize: 30)),
                  const SizedBox(width: 100),
                  ValueListenableBuilder<bool>( <-変更
                    valueListenable: settings.musicOn, <-変更
                    builder: (context, musicOn, child) { <-変更
                      return GestureDetector(
                          child: Icon(settings.musicOn.value
                              ? Icons.music_note
                              : Icons.music_off),
                          onTap: () => onTapAction(settings));
                    },
                  )
                ],
              )
            ],
          ),
          rectangularMenuArea: Column(
            children: [
              SizedBox(
                width: 100,
                child: FloatingActionButton(
                  onPressed: () {
                    GoRouter.of(context).go("/");
                  },
                  child: const Text('Back'),
                ),
              ),
            ],
          )),
    );
  }

  void onTapAction(SettingsState settings) {
    settings.toggleMusicOn();
  }
}

これで、ChangeNotifierからValueNotifierへの変更ができました。

AudioPlayerの導入

さて、設定画面の設定項目としてAudioPlayerパッケージを導入して、音楽再生機能を追加してみましょう。

シンプルに実装するなら、以下の記事が参考になると思います。

【Flutter】AudioPlayers で音声を再生する
FlutterでBGMや効果音を再生する方法について紹介します!今回は、AudioPlayersのパッケージを使用して音声を再生します。事前準備事前準備として、AudioPlayersのパッケージインストールや、音声ファイルの

ただ、これをそのまま実装すると例えば「音楽の再生をオンにするたびに同じ音が流される」や、「アプリをバックグラウンドにしても音楽が再生される」といった問題が発生します。

ここら辺の制御は自分で実装しようとするとかなり複雑になるので、フレームワークのソースコードからソースコードから極力変えずに実装していきましょう。

まずはAudioPlayerパッケージを使用している(らしき)クラスを自分のパッケージに入れてみましょう。で、最初のコンストラクタの宣言及びそこから呼ばれるメソッドを導入してみます。

my_audio_player.dart

class MyAudioPlayer {

  final AudioPlayer _musicPlayer;

  final List<AudioPlayer> _sfxPlayers;

  final Queue<Song> _playlist;

  SettingsState? _settings;

  ValueNotifier<AppLifecycleState>? _lifecycleNotifier;

  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}'));
  }
}

必要な処理のみ書いているのと、クラス名は変えているため適宜調整しています。気になる箇所を順次解説してみます。

  ValueNotifier<AppLifecycleState>? _lifecycleNotifier; 

先ほども説明していたValueNotifierがここでも使用されていますね。

AppLifecycleStateをValueNotifierで状態監視しています。AppLifecycleState自体は以前出てきましたね。アプリのライフサイクルを管理します(アプリを閉じたり開いたりした際の状態を表現するウィジェット)。

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);
  }

MyAudioPlayerのコンストラクタです。大体のケースはこれが最初に呼ばれると思われます。

ここで、2つのAudioPlayerを定義していますね。更にそれぞれのAudioPlayerインスタンスは見分けるためのキーを渡されています。

_sfxPlayersに入っているAudioPlayerインスタンスはIterable.generateで引数に渡されているpolyphony(今回は2)の数分だけ作成するようですね。

ちなみに、上記はコンストラクタ内でコロンで区切って複数の処理がカンマで区切られています。最初見た時何じゃこりゃ、って感じでしたがこれはどうやらInitializer listというものらしく、コンストラクタを呼び出した際に初期化処理として呼び出される処理のリストのようです。

で、Initializer Listの後のコンストラクタ自体の処理でmusicPlayerが終了した際のコールバック関数を定義しています。それが以下。

 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}'));
  }

Initializer List内で既にSongsウィジェットのリスト(曲名や曲のmp3ファイルのファイル名等を定義)を固定長で取得しています。このplayListの最初の曲を最後に持っていった上で再生していますね。

以上が音楽再生機能の初期設定です。ここにmain.dart内で設定されるパラメータを設定していきます。

まとめ

音楽再生機能ですが、ちょっと長くなりそうなので次の投稿に続けます。

コメント