address review: signal now playing with an event flow, not an int

This commit is contained in:
austinried
2026-10-04 00:30:07 +00:00
parent fd3b1ce298
commit b3cad97869
5 changed files with 57 additions and 15 deletions
@@ -9,13 +9,14 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.mutableIntStateOf
import androidx.core.content.ContextCompat
import com.subtracks.ui.NowPlayingLauncher
import com.subtracks.ui.SubtracksRoot
import com.subtracks.ui.theme.SubtracksTheme
import org.koin.core.context.GlobalContext
class MainActivity : ComponentActivity() {
private val nowPlayingRequest = mutableIntStateOf(0)
private val nowPlayingLauncher: NowPlayingLauncher by lazy { GlobalContext.get().get() }
private val requestNotifications =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { }
@@ -25,11 +26,11 @@ class MainActivity : ComponentActivity() {
requestNotificationPermission()
enableEdgeToEdge()
if (shouldRecordNowPlayingLaunch(intent?.action, restoringState = savedInstanceState != null)) {
nowPlayingRequest.intValue++
nowPlayingLauncher.request()
}
setContent {
SubtracksTheme {
SubtracksRoot(nowPlayingRequest = nowPlayingRequest.intValue)
SubtracksRoot()
}
}
}
@@ -37,7 +38,7 @@ class MainActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
if (intent.action == ACTION_OPEN_NOW_PLAYING) nowPlayingRequest.intValue++
if (intent.action == ACTION_OPEN_NOW_PLAYING) nowPlayingLauncher.request()
}
private fun requestNotificationPermission() {
@@ -28,6 +28,7 @@ import com.subtracks.playback.MediaSessionConnection
import com.subtracks.playback.PlaybackController
import com.subtracks.playback.PlayerConnection
import com.subtracks.playback.Scrobbler
import com.subtracks.ui.NowPlayingLauncher
import com.subtracks.ui.RootViewModel
import com.subtracks.ui.downloads.DownloadsViewModel
import com.subtracks.ui.home.HomeListViewModel
@@ -108,6 +109,7 @@ fun appModule(
it.attach(playback.state, playback.positionMs)
}
}
single { NowPlayingLauncher() }
viewModel { RootViewModel(get()) }
viewModel { LibraryViewModel(get(), get(), get(), get(), get(), get()) }
viewModel { HomeViewModel(get(), get(), get(), get()) }
@@ -181,10 +181,7 @@ internal fun focusPops(
}
@Composable
fun SubtracksRoot(
root: RootViewModel = koinViewModel(),
nowPlayingRequest: Int = 0,
) {
fun SubtracksRoot(root: RootViewModel = koinViewModel()) {
when (val hasSource = root.hasSource.collectAsStateWithLifecycle().value) {
null -> {
LoadingState()
@@ -198,14 +195,14 @@ fun SubtracksRoot(
val playbackController = koinInject<PlaybackController>()
LaunchedEffect(Unit) { playbackController.connect() }
val playbackReady by playbackController.ready.collectAsStateWithLifecycle()
if (playbackReady) MainNavigation(nowPlayingRequest) else LoadingState()
if (playbackReady) MainNavigation() else LoadingState()
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun MainNavigation(nowPlayingRequest: Int) {
private fun MainNavigation() {
val navController = rememberNavController()
val currentRoute =
navController
@@ -215,13 +212,13 @@ private fun MainNavigation(nowPlayingRequest: Int) {
?.route
val tabBarVisible = currentRoute == Routes.LIBRARY
val playbackController = koinInject<PlaybackController>()
val nowPlayingLauncher = koinInject<NowPlayingLauncher>()
val libraryRepository = koinInject<LibraryRepository>()
val downloadRepository = koinInject<DownloadRepository>()
val playback by playbackController.state.collectAsStateWithLifecycle()
val downloads by downloadRepository.states().collectAsStateWithLifecycle()
var showingQueue by rememberSaveable { mutableStateOf(false) }
var nowPlayingOpen by rememberSaveable { mutableStateOf(false) }
var handledNowPlayingRequest by remember { mutableStateOf(0) }
var nowPlayingProgress by remember { mutableFloatStateOf(0f) }
var miniPlayerTopPx by remember { mutableFloatStateOf(0f) }
var nowPlayingFadeOut by remember { mutableStateOf(false) }
@@ -306,9 +303,9 @@ private fun MainNavigation(nowPlayingRequest: Int) {
nowPlayingProgress = 0f
}
}
LaunchedEffect(nowPlayingRequest, playerVisible) {
if (playerVisible && nowPlayingRequest > handledNowPlayingRequest) {
handledNowPlayingRequest = nowPlayingRequest
LaunchedEffect(playerVisible) {
if (!playerVisible) return@LaunchedEffect
nowPlayingLauncher.openNowPlaying.collect {
if (navController.currentDestination?.route in DETAIL_ROUTES) {
navController.popBackStack(Routes.LIBRARY, inclusive = false)
}
@@ -0,0 +1,15 @@
package com.subtracks.ui
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
class NowPlayingLauncher {
private val requests = Channel<Unit>(Channel.CONFLATED)
val openNowPlaying: Flow<Unit> = requests.receiveAsFlow()
fun request() {
requests.trySend(Unit)
}
}
@@ -0,0 +1,27 @@
package com.subtracks.ui
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeoutOrNull
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class NowPlayingLauncherTest {
@Test
fun aRequestMadeBeforeCollectionIsDelivered() =
runTest {
val launcher = NowPlayingLauncher()
launcher.request()
assertEquals(Unit, launcher.openNowPlaying.first())
}
@Test
fun repeatedRequestsCollapseToTheLatest() =
runTest {
val launcher = NowPlayingLauncher()
repeat(3) { launcher.request() }
assertEquals(Unit, launcher.openNowPlaying.first())
assertNull(withTimeoutOrNull(100) { launcher.openNowPlaying.first() })
}
}