I got a local object that I am creating in compose...
# compose
s
I got a local object that I am creating in compose which requires cleanup when it is destroyed. Currently I am doing:
Copy code
val lifecycleOwner = LocalLifecycleOwner.current
var audioPlayer: AudioPlayer? by remember {
    mutableStateOf(null)
}
DisposableEffect(audioUrl, lifecycleOwner) {
    val localAudioPlayer = AudioPlayer(audioUrl, lifecycleOwner)
    audioPlayer = localAudioPlayer
    onDispose {
        localAudioPlayer.cleanup()
    }
}
So using DisposableEffect to make sure to cleanup the AudioPlayer in case any of the keys change and I create a new instance of AudioPlayer so I cleanup the old one, or if it leaves the composition. This feels a bit hacky however, especially with the initial null state above it, making the code below this awkward too. Is there some better approach I could possibly look into instead?
d
Yes there is! One sec
s
Ah super neat! Is this
java.io.Closeable
?
d
Indeed
s
Super smart, thank you for this!
And adding the keys should be as simple as this right
Copy code
@Composable
fun <T : Closeable> rememberCloseable(
    key1: Any?,
    key2: Any?,
    calculation: () -> T,
): T {
    val wrapper = remember(key1, key2) { Wrapper(calculation()) }
    return wrapper.obj
}
d
Yup
🙏 1
z
One thing to be cautious about with remember observer - if you're creating something that is expensive to initialize, be aware that the observer can be remembered and then abandoned without ever being used to display anything, and that at could happen many times or with a very short time in between.
s
But is there any alternative without doing what I did above and have to deal with the nullable value? By "be cautious" do you mean "be very cautious each time you use this" or basically don't think about it until it really becomes an obvious problem?
z
Just something to keep in mind. E.g. if you’re creating and tearing down a network connection in a remember observer, you might have a bad time.
s
Is there a way I can emulate what you’re describing in order to test how my use case would behave? I’m initializing a MediaPlayer which technically does do a network request since it gets an audio file from a remote source 🤔 This does make me worry then. Maybe I could move creating the MediaPlayer outside of the object initialization and into some sort of
initialize
function that I can call later and not during object creation. That could help in this case right?
z
That could work. In general performing side effects, especially expensive ones, in constructors is an anti-pattern for exactly this reason.
But i’m guessing your media player is coming from a library or something
s
Yes it’s just the Android MediaPlayer, nothing I’ve made myself. Remembering it in here in the class that just initializes it directly as of right now and closing it on
close
coming from the
Closeable
interface. I think I have to do what we said yeah, initialize it later. Maybe as a
Copy code
LaunchedEffect(audioPlayer) {
  audioPlayer.initialize()
}
But I am not sure if this would behave the same way as you said before, re-trigger multiple times. And if it does, where else would that fit while we’re inside the context of a composable?
z
It won't fire multiple times, a launched effect's coroutine starts when the effect enters the composition and isn't cancelled until it is removed or a key changes.
I would probably make
initialize
a suspend function so it can do its own cleanup, and then call it something other than initialize to indicate it's long-running (maybe
run
?) Or change it to an initialize/dispose pair and use DisposableEffect like you had before.
s
So 1:
Copy code
class AudioPlayer(key1: Any?, key2: Any?) {
    fun initialize() { TODO() }
    fun cleanup() { TODO() }
}

@Composable
fun Something(key1: Any?, key2: Any?) {
    val audioPlayer = remember(key1, key2) {
        AudioPlayer(key1, key2)
    }
    DisposableEffect(audioPlayer) {
        audioPlayer.initialize()
        onDispose {
            audioPlayer.cleanup()
        }
    }
}
2:
Copy code
class AudioPlayer2(key1: Any?, key2: Any?) {
    suspend fun activate() {
        initialize()
        try {
            awaitCancellation() // Is this the best way to do this?
        } finally {
            cleanup()
        }
    }
    private fun initialize() { TODO() }
    private fun cleanup() { TODO() }
}

@Composable
fun Something2(key1: Any?, key2: Any?) {
    val audioPlayer = remember(key1, key2) {
        AudioPlayer2(key1, key2)
    }
    LaunchedEffect(audioPlayer) {
        audioPlayer.activate()
    }
}
3:
Copy code
class AudioPlayer3(key1: Any?, key2: Any?) : Closeable {
    fun initialize() {}
    override fun close() {}
}

@Composable
fun Something3(key1: Any?, key2: Any?) {
    val lifecycleOwner = LocalLifecycleOwner.current
    val audioPlayer = rememberCloseable(key1, key2) {
        AudioPlayer3(key1, key2)
    }
    LaunchedEffect(audioPlayer) {
        audioPlayer.initialize()
    }
}
Would all be reasonable with their own pro/cons in terms of how easy they are to use on the call site. With 1 being the most explicit on the call site about when we are initializing it and disposing it. 2 probably being the most concise on the call site. 3 introducing the new concept of rememberCloseable which may not be universally understood? If all those are correct and hide no bugs idk which one to pick tbh. But might be that not all of them behave exactly the same, what do you think?
z
I'm not familiar with
rememberCloseable
but the first two look good.
d
Personally, after seeing all these warnings, I'd have gone with your initial nullable approach and added a progress bar or something.
s
Yeah that's a lot of warnings indeed 😅 at this point I'm just exploring all options to try and learn as much as I can, but I'll go with the disposable option probably for safety and clarity