TheFlame version of Langaw has been released to the Google Play Store. The number one negative feedback I got was that the background music keeps on playing even after the game is closed.

The game has been updated and the background music issue has been fixed but the fix was a custom and non-reusable.
In this article, I’d like to share a class that you can just paste in your Flame game project and use immediately to manage and play looping background music files.
If you want to skip the boring part and just want to add background music to your games, skip to the “how to use“ section below.
Let’s Get Started
We would need a class that will manage the background music. All its properties and methods will be  static  so we don’t need to create an instance of the class to use it.
Create a class in  ./lib/bgm.dart  and add the following code:
import 'package:audioplayers/audio_cache.dart';
class BGM {
  static List<AudioCache> _tracks = List<AudioCache>();
  static int _currentTrack = -1;
  static bool _isPlaying = false;
}
Note: The location  ./  refers to the root directory of the project. It should be where the file  pubspec.yaml  should be.
Breakdown: Import statement at the top just gives us access to the  AudioCache  class from the  audioplayers  library. What follows is a standard class declaration. The class is named  BGM  and has three static properties. Static properties (and methods) are like global properties but only for that class. It’s like changing a property of the class itself instead of changing a property of an instance of the class.
All these properties are private though, properties or methods that start with an underscore (_) are private and can only be changed/accessed from within the  BGM  class itself.
Before all other functionalities, let’s add an update method first. This method will contain the code for managing the state (playing or pausing) the track.
static Future _update() async {
  if (_currentTrack == -1) {
    return;
  }
  if (_isPlaying) {
    await _tracks[_currentTrack].fixedPlayer.resume();
  } else {
    await _tracks[_currentTrack].fixedPlayer.pause();
  }
}
Breakdown: This is method is rather simple. If  _currentTrack  is set to  -1, it means no track is playing. In this case, the method just  returns and ends.
If  _currentTrack  is set to something else, the method checks for the value of  _isPlaying. If it’s true, the method then calls  resume  on the track’s  fixedPlayer  object. Otherwise, the method calls  pause.
Resource management
In a perfect world, we could just load all our audio assets into memory ready to be played at a moment’s notice. But electronic devices have limited memory, not to mention they probably don’t run just a single app or game.

Knowing this, it’s our responsibility as game developers to limit the resources we use to a minimum loading only the things we’re going to need inside a specific view.
Back to the code, we will need access to the  AudioPlayer  class and the  ReleaseMode  enum  that are defined in the  audioplayers.dart  file of the  audioplayers  library. Let’s import that file with the following line:
import 'package:audioplayers/audioplayers.dart';
Then let’s add a method called  add  to the class. This method will also be static. I/O operations are involved so we’re also making this method asynchronous.
static Future<void> add(String filename) async {
  AudioCache newTrack = AudioCache(prefix: 'audio/', fixedPlayer: AudioPlayer());
  await newTrack.load(filename);
  await newTrack.fixedPlayer.setReleaseMode(ReleaseMode.LOOP);
  _tracks.add(newTrack)
}
Not much of a breakdown: This method involves preloading a background music track from a file. The specifics of each line gets a bit more in-depth with the  audioplayers  package. For now, just know that what’s happening is we’re creating a new instance of the  AudioCacheclass and adding it to the  _tracks  list.
The method above could be called at the beginning (entry point) of the game to preload resources or on loading screens based on how complex the game you’re developing is.
Now that audio resources can be added to the cache, we should have a way to remove those resources from the memory.
static void remove(int trackIndex) async {
  if (trackIndex >= _tracks.length) {
    return;
  }
  if (_isPlaying) {
    if (_currentTrack == trackIndex) {
      await stop();
    }
    if (_currentTrack > trackIndex) {
      _currentTrack -= 1;
    }
  }
  _tracks.removeAt(trackIndex);
}
Note: We’re calling on the  stop  method here which doesn’t exist yet. We’ll write it into the class later in the next section.
Breakdown: The method above accepts an  int  (trackIndex) as a parameter and checks if that index exists. If  trackIndex  is greater or equal to the number of items in  _tracks  list, the method just  returns and ends.
Next, it checks if a BGM is currently playing. If  _isPlaying  is  true  and the currently playing BGM track is the one being removed, call  stopfirst. If the currently playing BGM track is lower in the list than the one being removed, we need to update the  _currentTrack  variable so its value will point correctly to the new index after removing an item above it.
Finally, we remove the track from the  _tracks  list.
There are scenarios where you want to just unload all BGM tracks and start over. This is useful if you’re changing scenes with a totally different mood or when going back to the main menu.
Let’s write a method to do just that.
static void removeAll() {
  if (_isPlaying) {
    stop();
  }
  _tracks.clear();
}
Breakdown: First, we  stop  the currently playing BGM track if there’s any. After that, we clear the  _tracks  list. That’s it.
Play, stop, pause, and resume
To control the BGM tracks, we write the following methods.
The first method is  play. This method accepts an  int  named  trackIndex  as a parameter that specifies which track should be played.
static Future play(int trackIndex) async {
  if (_currentTrack == trackIndex) {
    if (_isPlaying) {
      return;
    }
    _isPlaying = true;
    _update();
    return;
  }
  if (_isPlaying) {
    await stop();
  }
  _currentTrack = trackIndex;
  _isPlaying = true;
  AudioCache t = _tracks[_currentTrack];
  await t.loop(t.loadedFiles.keys.first);
  _update();
}
Breakdown: The first thing that the  play  method does is check if the supplied  trackIndex  is the same as the currently playing track index. If it’s a match and it’s currently playing, the method immediately ends through a  return. If it matches but isn’t playing (I can’t really imagine how this scenario could happen, but let’s just put here as a safety net), set the variable  _isPlaying  to  true  and call  _update()  then end through a  return.
If the passed  trackIndex  value is not the same as the current one, it means that the playing track is being changed (even if there isn’t one playing currently). A check is done if there is something playing by checking the variable  _isPlaying‘s value. If something is playing,  stop  is called to make sure the other track is stopped properly.
Note: Again, the  stop  method does not exist yet, but we’ll get to shortly (right after this breakdown actually).
After all the preparation is done, we set the  _currentTrack  value to whatever was passed on  trackIndex and set  _isPlaying  to  true.
Here’s the most confusing bit, first we get the actual  AudioCache  “track” and assign it to the variable named  t. We then call the  loop  method of this track and pass in the filename of the first loaded file. To clear things up, when we call the  add  method above, it adds a new track (AudioCacheobject) to the list. Each track holds a cache of loaded music files in a  Map  named  loadedFiles  where the keys are stored as  Strings. For each cached music file, the key is set to the filename you supply to it. In this class, one track only holds/caches one music file so we can get the original filename passed by accessing  .keys.first  of the  loadedFilesproperty.
To know more about Dart  Maps,  check out the  Map  documentation.
Now let’s go to the  stop  method.
static Future stop() async {
  await _tracks[_currentTrack].fixedPlayer.stop();
  _currentTrack = -1;
  _isPlaying = false;
}
Breakdown: The method simply calls  stop  on the track’s  fixedPlayerproperty. This method is more on the  audioplayers  package side so  check out the docs  if you want to learn more about it.
Next, we have  pause.
static void pause() {
  _isPlaying = false;
  _update();
}
And lastly,  resume.
static void resume() {
  _isPlaying = true;
  _update();
}
Breakdown: The  pause  and  resume  methods just set the value of  _isPlaying  to  false  and  true, respectively. After that,  _update  is called and it handles pausing and resuming there.
Solving the background music problem
We now have a class that can manage resources (caching and preloading) and control tracks (play, stop, pause, resume).

Yay! Yeah… no.
The original problem was that the music kept on playing even after exiting the game or switching from the game to another app.
How do we solve this?
We extend a  WidgetsBindingObserver  and listen to app lifecycle state changes.
Let’s create another class.
class _BGMWidgetsBindingObserver extends WidgetsBindingObserver {
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      BGM.resume();
    } else {
      BGM.pause();
    }
  }
}
Breakdown: This class extends  WidgetsBindingObserver  and overrides the method  didChangeAppLifecycleState. This method is fired every time the lifecycle state of the app is changed. The new state is passed as a parameter. If the app just resumed,  BGM.resume()  is called. For all other states,  BGM.pause()  is called.
This class by itself does nothing, we need to create an instance of it first and attach it as an observer to the  WidgetsBinding  instance. Besides, this class is private (its name starts with an underscore) so it’s no use outside this file.
The  BGM  class, on the other hand, is a static global singleton, supposedly anyway. So back to the  BGM  class, let’s add a private property and a getter for that property.
static _BGMWidgetsBindingObserver _bgmwbo;
static _BGMWidgetsBindingObserver get widgetsBindingObserver {
  if (_bgmwbo == null) {
    _bgmwbo = _BGMWidgetsBindingObserver();
  }
  return _bgmwbo;
}
Breakdown: We’re adding two class members here, a private property (_bgmwbo) and a getter (widgetsBindingObserver) which returns the private property. We use this  private property–getter  combo so we can store instantiate a value if it’s null and store it for referencing later. In the end, if we access  widgetsBindingObserver, we’re actually getting the  _bgmwbo  property. This setup just gives the class a chance to initialize it first.
Next, let’s add a helper function that will bind our observer into the  WidgetsBinding  instance.
static void attachWidgetBindingListener() {
  WidgetsBinding.instance.addObserver(BGM.widgetsBindingObserver);
}
Breakdown: It’s a pretty standard one-liner. We get the  WidgetsBindinginstance and call its  addObserver  method passing  BGM.widgetsBindingObserver  (which is a getter that initializes an instance of  _BGMWidgetsBindingObserver  if there’s none).
Now we have a working, reusable class file that deals with looping BGM tracks.
How to use in your Flame game
First thing’s first, you must have this file in an easily accessible location. Having this file in  ./lib/bgm.dart  works best. But it’s really up to you. Having it in  ./lib/globals/bgm.dart, for example, works too.
Here’s the whole class file:
import 'package:audioplayers/audio_cache.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/widgets.dart';
class BGM {
  static List _tracks = List();
  static int _currentTrack = -1;
  static bool _isPlaying = false;
  static _BGMWidgetsBindingObserver _bgmwbo;
  static _BGMWidgetsBindingObserver get widgetsBindingObserver {
    if (_bgmwbo == null) {
      _bgmwbo = _BGMWidgetsBindingObserver();
    }
    return _bgmwbo;
  }
  static Future _update() async {
    if (_currentTrack == -1) {
      return;
    }
    if (_isPlaying) {
      await _tracks[_currentTrack].fixedPlayer.resume();
    } else {
      await _tracks[_currentTrack].fixedPlayer.pause();
    }
  }
  static Future add(String filename) async {
    AudioCache newTrack = AudioCache(prefix: 'audio/', fixedPlayer: AudioPlayer());
    await newTrack.load(filename);
    await newTrack.fixedPlayer.setReleaseMode(ReleaseMode.LOOP);
    _tracks.add(newTrack);
  }
  static void remove(int trackIndex) async {
    if (trackIndex >= _tracks.length) {
      return;
    }
    if (_isPlaying) {
      if (_currentTrack == trackIndex) {
        await stop();
      }
      if (_currentTrack > trackIndex) {
        _currentTrack -= 1;
      }
    }
    _tracks.removeAt(trackIndex);
  }
  static void removeAll() {
    if (_isPlaying) {
      stop();
    }
    _tracks.clear();
  }
  static Future play(int trackIndex) async {
    if (_currentTrack == trackIndex) {
      if (_isPlaying) {
        return;
      }
      _isPlaying = true;
      _update();
      return;
    }
    if (_isPlaying) {
      await stop();
    }
    _currentTrack = trackIndex;
    _isPlaying = true;
    AudioCache t = _tracks[_currentTrack];
    await t.loop(t.loadedFiles.keys.first);
    _update();
  }
  static Future stop() async {
    await _tracks[_currentTrack].fixedPlayer.stop();
    _currentTrack = -1;
    _isPlaying = false;
  }
  static void pause() {
    _isPlaying = false;
    _update();
  }
  static void resume() {
    _isPlaying = true;
    _update();
  }
  static void attachWidgetBindingListener() {
    WidgetsBinding.instance.addObserver(BGM.widgetsBindingObserver);
  }
}
class _BGMWidgetsBindingObserver extends WidgetsBindingObserver {
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      BGM.resume();
    } else {
      BGM.pause();
    }
  }
}
Setup
In your game’s  ./lib/main.dart, import the BGM class file:
import 'bgm.dart';
Or if it’s under globals:
import 'globals/bgm.dart';
Note: You could use the more common import format which specifies your package name and a full path to the file. Again, it’s up to you.
Then have the following line AFTER calling  runApp:
BGM.attachWidgetBindingListener();
Preloading (and unloading)
Preloading/adding tracks:
await BGM.add('bgm/awesome-intro.ogg');
await BGM.add('bgm/flame-game-level.ogg');
await BGM.add('bgm/boss-fight.ogg');
Unloading all tracks (freeing the memory from BGM tracks):
BGM.removeAll();
Rarely, you may want to unload a single track (let’s say you won’t need the boss fight track anymore):
BGM.remove(2);
Playing (or changing tracks)
To play a track, just call:
// intro or home screen
BGM.play(0);
// boss fight is about to start
BGM.play(2);
It’s okay if a different track is currently playing. The class will handle it by stopping the currently playing one and playing the one specified (as you can see in the code above).
Stopping
To stop the current track, just call:
await BGM.stop();
Note: Some of the function calls above need to be added inside an  async  function if you care about timing (you should).
Conclusion
That was it. Have fun making games with awesome background music tracks that automatically stop when you exit the game or switch to a different app.
If you have any questions, contact me with an email, drop a comment below, or join my Discord server.
Thank you japalekhin! You've just received an upvote of 40% by artturtle!
Learn how I will upvote each and every one of your posts
Please come visit me to see my daily report detailing my current upvote power and how much I'm currently upvoting.