Search code examples
androidandroid-jetpack-compose

ComposeView doesn't always refresh when composable content recomposes


I am absolutely baffled by this: I want to show a composable miniplayer in a ComposeView (in the layout.xml of a Fragment) as the rest of the screen (apart from one other ComposeView) is still in xml. The miniplayer recomposes just fine when state changes but this isn't always reflected on screen as sometimes the view doesn't update when state changes but if I run the app again it might just work as expected (or not).

What I have tried:

  • Verifying the state does indeed update as expected (it does)
  • Verifying the composable recomposes (it does and even when the state updates the UI doesn't always change but it does recompose)
  • Verifying the actual song chosen does not cause this issue (same song/data sometimes works and most of the times doesn't work so it's unrelated)
  • Verifying if the same composable works in a fully composable screen (it does)
  • Updating the compose BOM to the latest version (was using 2024.05.00 and now 2024.08.00)
  • Updating Android Studio
  • Fiddling with the view composition strategy of the ComposeView
  • Using observeAsState() instead of observeAsStateWithLifecycle()

Some code:

Part of the XML layout containing the ComposeView


    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/miniPlayerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

How I put the composable content in the ComposeView

    ui.miniPlayerView.setContent {
        AppTheme {
            MiniAudioPlayer()
        }
    }

The composable

private const val MINIMUM_DRAG_AMOUNT = -100f

@Composable
fun MiniAudioPlayer(
    modifier: Modifier = Modifier,
    playerHandler: PlayerHandler = koinInject()
) {
    val context = LocalContext.current
    val state by playerHandler.miniPlayerState.collectAsStateWithLifecycle()

    val progress by remember { derivedStateOf { state.progress } }
    val showPlayer by remember { derivedStateOf { state.showPlayer } }
    val isPlaying by remember { derivedStateOf { state.isPlaying } }
    val title by remember { derivedStateOf { state.title } }
    val thumbnailUrl by remember { derivedStateOf { state.thumbnailUrl } }
    var totalDragAmount by remember { mutableFloatStateOf(0f) }
    val interactionSource = remember { MutableInteractionSource() }

    if (showPlayer) {
        Row(
            modifier = modifier
                .clickable(interactionSource = interactionSource, indication = null) { context.openAudioPlayer() }
                .pointerInput(Unit) {
                    detectVerticalDragGestures(
                        onDragStart = { totalDragAmount = 0f },
                    ) { _, dragAmount ->
                        totalDragAmount += dragAmount
                        if (totalDragAmount < MINIMUM_DRAG_AMOUNT) context.openAudioPlayer()
                    }
                }
                .background(color = LocalCustomColors.current.background)
                .background(color = colorResource(R.color.miniplayer_background))
                .padding(16.dp),
        ) {
            MMImage(
                url = thumbnailUrl,
                size = DpSize(54.dp, 54.dp),
                modifier = Modifier
                    .size(54.dp)
                    .clip(RoundedCornerShape(8.dp))
            )
            Spacer(modifier = Modifier.width(12.dp))
            Column(
                modifier = Modifier
                    .weight(1f)
            ) {
                Spacer(modifier = Modifier.height(8.dp))
                Header5Text(
                    text = title,
                    maxLines = 1
                )
                Spacer(modifier = Modifier.height(10.dp))
                MMProgressBar(progress = { progress })
            }
            Spacer(modifier = Modifier.width(18.dp))
            MMIcon(
                drawableId = if (isPlaying) R.drawable.ic_miniplayer_pause_button else R.drawable.ic_miniplayer_play_button,
                size = 34.dp,
                modifier = Modifier
                    .clickable(interactionSource = interactionSource, indication = null) {
                        playerHandler.togglePlayState()
                    }
                    .align(Alignment.CenterVertically)
            )
        }
    }
}

How I update the values in the ViewModel (PlayerHandler class)

private val _miniPlayerState = MutableStateFlow(MiniPlayerUiState())
val miniPlayerState = _miniPlayerState.asStateFlow()

_miniPlayerState.update { it.copy(progress = progress / 100f) }

The state

data class MiniPlayerUiState(
    val showPlayer: Boolean = false,
    val isPlaying: Boolean = false,
    val progress: Float = 0f,
    val title: String = "",
    val thumbnailUrl: String? = null
)

Solution

  • Turns out this issue was caused by having strong skipping mode enabled in the compose compiler options... I guess ComposeViews are not (yet?) compatible with strong skipping mode.

    composeCompiler {
        enableStrongSkippingMode = true
    
        ...
    }