Reviews onDispose calls to make sure the onDispose is disposing the data from the previous composable, since it can happen AFTER a new composition already took place. So the references must match the old composition.

This commit is contained in:
Vitor Pamplona
2025-07-03 10:27:47 -04:00
parent 5bcaf065a3
commit 69db9bdcd5
13 changed files with 97 additions and 80 deletions
@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.service.playback.composable package com.vitorpamplona.amethyst.service.playback.composable
import android.view.View
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -67,8 +68,19 @@ fun ControlWhenPlayerIsActive(
// Keeps the screen on while playing and viewing videos. // Keeps the screen on while playing and viewing videos.
DisposableEffect(key1 = controller, key2 = view) { DisposableEffect(key1 = controller, key2 = view) {
val listener = val listener = PlayerEventListener(view)
object : Player.Listener {
controller.addListener(listener)
onDispose {
controller.removeListener(listener)
listener.destroy()
}
}
}
class PlayerEventListener(
val view: View,
) : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
// doesn't consider the mutex because the screen can turn off if the video // doesn't consider the mutex because the screen can turn off if the video
// being played in the mutex is not visible. // being played in the mutex is not visible.
@@ -76,14 +88,10 @@ fun ControlWhenPlayerIsActive(
view.keepScreenOn = isPlaying view.keepScreenOn = isPlaying
} }
} }
}
controller.addListener(listener) fun destroy() {
onDispose {
if (view.keepScreenOn) { if (view.keepScreenOn) {
view.keepScreenOn = false view.keepScreenOn = false
} }
controller.removeListener(listener)
}
} }
} }
@@ -54,6 +54,10 @@ fun GetVideoController(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// Prepares a VideoPlayer from the foreground service. // Prepares a VideoPlayer from the foreground service.
//
// TODO: Review this code because a new Disposable Effect can run
// before the onDispose of the previous composable and the onDispose
// sometimes affects the new variables, not the old ones.
DisposableEffect(key1 = mediaItem.src.videoUri) { DisposableEffect(key1 = mediaItem.src.videoUri) {
// If it is not null, the user might have come back from a playing video, like clicking on // If it is not null, the user might have come back from a playing video, like clicking on
// the notification of the video player. // the notification of the video player.
@@ -56,8 +56,11 @@ fun Waveform(
val restartFlow = remember { mutableIntStateOf(0) } val restartFlow = remember { mutableIntStateOf(0) }
val myController = mediaControllerState.controller
// Keeps the screen on while playing and viewing videos. // Keeps the screen on while playing and viewing videos.
DisposableEffect(key1 = mediaControllerState.controller) { if (myController != null) {
DisposableEffect(key1 = myController) {
val listener = val listener =
object : Player.Listener { object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
@@ -69,8 +72,9 @@ fun Waveform(
} }
} }
mediaControllerState.controller?.addListener(listener) myController.addListener(listener)
onDispose { mediaControllerState.controller?.removeListener(listener) } onDispose { myController.removeListener(listener) }
}
} }
LaunchedEffect(key1 = restartFlow.intValue) { LaunchedEffect(key1 = restartFlow.intValue) {
@@ -39,7 +39,7 @@ object BackgroundMedia {
fun removeBackgroundControllerAndReleaseIt() { fun removeBackgroundControllerAndReleaseIt() {
bgInstance.value?.let { bgInstance.value?.let {
PlaybackServiceClient.removeController(it) PlaybackServiceClient.removeController(it)
clearBackground() bgInstance.tryEmit(null)
} }
} }
@@ -47,7 +47,9 @@ object BackgroundMedia {
bgInstance.tryEmit(mediaControllerState) bgInstance.tryEmit(mediaControllerState)
} }
fun clearBackground() { fun clearBackground(mediaControllerState: MediaControllerState) {
if (bgInstance.value == mediaControllerState) {
bgInstance.tryEmit(null) bgInstance.tryEmit(null)
} }
}
} }
@@ -58,9 +58,8 @@ fun rememberIsInPipMode(): Boolean {
Consumer<PictureInPictureModeChangedInfo> { info -> Consumer<PictureInPictureModeChangedInfo> { info ->
pipMode = info.isInPictureInPictureMode pipMode = info.isInPictureInPictureMode
} }
activity.addOnPictureInPictureModeChangedListener(
observer, activity.addOnPictureInPictureModeChangedListener(observer)
)
onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) } onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) }
} }
return pipMode return pipMode
@@ -81,7 +81,7 @@ fun PipVideo(controller: MediaControllerState) {
DisposableEffect(controller) { DisposableEffect(controller) {
BackgroundMedia.switchKeepPlaying(controller) BackgroundMedia.switchKeepPlaying(controller)
onDispose { onDispose {
BackgroundMedia.clearBackground() BackgroundMedia.clearBackground(controller)
} }
} }
@@ -40,7 +40,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -74,12 +73,6 @@ fun NewUserMetadataScreen(
postViewModel.load(accountViewModel.account) postViewModel.load(accountViewModel.account)
} }
DisposableEffect(Unit) {
onDispose {
postViewModel.clear()
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -104,7 +104,6 @@ fun keyboardAsState(): State<Keyboard> {
} }
} }
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) } onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) }
} }
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.ViewModelStoreOwner
@@ -55,11 +54,13 @@ fun SetAccountCentricViewModelStore(
content() content()
} }
DisposableEffect(key1 = state) { // moved this clearing activity to the viewmodel account
onDispose { // because the new composable might run before the onDispose.
state.currentViewModelStore.viewModelStore.clear() // DisposableEffect(key1 = state) {
} // onDispose {
} // state.currentViewModelStore.viewModelStore.clear()
// }
// }
} }
class AccountCentricViewModelStore : ViewModelStoreOwner { class AccountCentricViewModelStore : ViewModelStoreOwner {
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.util.Log import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -216,9 +217,7 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
} }
} }
lifeCycleOwner.lifecycle.addObserver(observer) val launcher: (Intent) -> Unit = {
accountViewModel.account.signer.launcher.registerLauncher(
launcher = {
try { try {
launcher.launch(it) launcher.launch(it)
} catch (e: Exception) { } catch (e: Exception) {
@@ -229,12 +228,16 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
R.string.error_opening_external_signer_description, R.string.error_opening_external_signer_description,
) )
} }
}, }
lifeCycleOwner.lifecycle.addObserver(observer)
accountViewModel.account.signer.launcher.registerLauncher(
launcher = launcher,
contentResolver = Amethyst.instance::contentResolverFn, contentResolver = Amethyst.instance::contentResolverFn,
) )
onDispose { onDispose {
accountViewModel.account.signer.launcher accountViewModel.account.signer.launcher
.clearLauncher() .clearLauncherIf(launcher)
lifeCycleOwner.lifecycle.removeObserver(observer) lifeCycleOwner.lifecycle.removeObserver(observer)
} }
} }
@@ -64,8 +64,6 @@ fun TabRelays(
lifeCycleOwner.lifecycle.addObserver(observer) lifeCycleOwner.lifecycle.addObserver(observer)
onDispose { onDispose {
lifeCycleOwner.lifecycle.removeObserver(observer) lifeCycleOwner.lifecycle.removeObserver(observer)
println("Profile Relay Dispose")
feedViewModel.unsubscribeTo(user)
} }
} }
@@ -641,8 +641,7 @@ private fun PrepareExternalSignerReceiver(onLogin: (pubkey: String, packageName:
val activity = getActivity() as MainActivity val activity = getActivity() as MainActivity
DisposableEffect(launcher, activity, externalSignerLauncher) { DisposableEffect(launcher, activity, externalSignerLauncher) {
externalSignerLauncher.registerLauncher( val launcher: (Intent) -> Unit = {
launcher = {
try { try {
launcher.launch(it) launcher.launch(it)
} catch (e: Exception) { } catch (e: Exception) {
@@ -657,10 +656,15 @@ private fun PrepareExternalSignerReceiver(onLogin: (pubkey: String, packageName:
).show() ).show()
} }
} }
}, }
externalSignerLauncher.registerLauncher(
launcher = launcher,
contentResolver = Amethyst.instance::contentResolverFn, contentResolver = Amethyst.instance::contentResolverFn,
) )
onDispose { externalSignerLauncher.clearLauncher() } onDispose {
externalSignerLauncher.clearLauncherIf(launcher)
}
} }
LaunchedEffect(externalSignerLauncher) { LaunchedEffect(externalSignerLauncher) {
@@ -114,10 +114,12 @@ class ExternalSignerLauncher(
} }
/** Call this function when the activity is destroyed or is about to be replaced. */ /** Call this function when the activity is destroyed or is about to be replaced. */
fun clearLauncher() { fun clearLauncherIf(launcher: ((Intent) -> Unit)) {
if (signerAppLauncher == launcher) {
this.signerAppLauncher = null this.signerAppLauncher = null
this.contentResolver = null this.contentResolver = null
} }
}
fun newResult(data: Intent) { fun newResult(data: Intent) {
val results = data.getStringExtra("results") val results = data.getStringExtra("results")