Handling dispose method when using hot restart

Is there a way to properly dispose resources when using hot restart:

Hot restart loads code changes into the VM, and restarts the Flutter app, losing the app state. (⇧⌘\ in IntelliJ and Android Studio, ⇧⌘F5 in VSCode)

In my app, I have a heavy resource (here a audio player) which is created on initState then disposed in dispose.

When tapping the button, it will start playing a song:

import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: const MyHomePage()));
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late AudioPlayer audioPlayer;

  @override
  void initState() {
    print("Creating audio player");
    audioPlayer = AudioPlayer();
    super.initState();
  }

  @override
  void dispose() {
    print("Disposing audio player");
    audioPlayer.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          print("Starting new song");
          final result = await audioPlayer.play(
            'https://upload.wikimedia.org/wikipedia/commons/c/c2/Toccata_and_Fugue_in_D_Minor_%28ISRC_USUAN1100350%29.mp3',
          );
          await audioPlayer.resume();
          print(result);
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

The issue is when hot restart is used, the dispose method is not called, so the song playing before the hot restart does not stop and tapping the button will start playing another song over the top of the last song.

What is the correct way to dispose an object when hot restart is used given dispose is not called?

My pubspec is the default pubspec with audioplayers dependency added:

name: flutter_application_1
description: A new Flutter project.
version: 1.0.0+1
environment:
  sdk: ">=2.16.0-10.0.dev <3.0.0"
dependencies:
  flutter:
    sdk: flutter
  audioplayers: ^0.20.1
dev_dependencies:
  flutter_test:
    sdk: flutter

Unfortunately, there's no simple way to fix this.

Hot-restarting your app is basically like killing it's process and starting it again (with updated code), but witout actual native process being killed. This implies that app being killed because of hot-restart cannot execute any cleanup code (hence no equivalent of reassemble method of hot reload).

Unfortunately, platform plugins are not notified of hot-restart (there's an open issue in flutter engine, you can upvote it), so they also cannot cleanup resources acquired by app being hot-restarted.

One possible workaround would be to release all previously acquired resources on app startup. On first startup nothing would be released, after hot restart app would release resources from the previous run. Unfortunately, audioplayers (and other libraries like flutter_sound) does not expose any global "disposeAll" API.

So, tl;dr, best solution is the one that @PatrickMahomes provided, add a development-time button to stop the players before hot-restart (or just ignore the problem since it will never come up in a release build). But, if you for some reason would really want to do it, here is a

Ugly and insane hack that I would absolutely not recommend implementing:

One way to make this work in audioplayers, would be to save IDs of plaing players (when they're being started, not when the app is being killed) to some persistent storage, read them when app is being started (as described above) and call AudioPlayer(playerId: id).release() on every of them.


The problem is caused by Hot-Realoading not beeing able to notify the plugin about hot-restart.

This solution is just a workaround for your specific situation.

I would save the player ID as soon as you initialise the player:

Future<AudioPlayer> _initPlayer() async {
    print("Creating audio player");
    SharedPreferences prefs = await SharedPreferences.getInstance();
    final playerId = prefs.getString('audioPlayer');
    if (playerId != null) {
      AudioPlayer(playerId: playerId).release();
    }
    final audioPlayer = AudioPlayer();
    await prefs.setString('audioPlayer', audioPlayer.playerId);
    return audioPlayer;
  }

This function uses SharedPreferences to store the playerId. Each time you init the player, this code checks for a stored ID, if there is one it creates an AudioPlayer with that ID and releases it. Then it proceeds creating a new one.

Since this function is asynchronous, it cannot be run inside the initState().

I wrapped the FloatingActionButton inside a FutureBuilder linked to the _initPlayer() function.

 @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FutureBuilder(
        future: _initPlayer(),
        builder: (BuildContext context, AsyncSnapshot<AudioPlayer> snapshot) {
          if (snapshot.hasData) {
            audioPlayer = snapshot.data!;
            return FloatingActionButton(
              onPressed: () async {
                print("Starting new song");
                await audioPlayer.play(
                  'https://upload.wikimedia.org/wikipedia/commons/c/c2/Toccata_and_Fugue_in_D_Minor_%28ISRC_USUAN1100350%29.mp3',
                );
              },
              child: const Icon(Icons.play_arrow),
            );
          }

          return FloatingActionButton(
            onPressed: () => {},
            child: const CircularProgressIndicator(),
          );
        },
      ),
    );
  }