feat(android): OutputPickerViewModel - sheet lifecycle + selection
android / Build + lint + test (push) Has been cancelled

Bluetooth slice (3/5). HiltViewModel projecting the controller's
routesState Flow plus a sheetVisible MutableStateFlow that owns
the sheet's open/close state. onChipTapped + onSheetDismissed
forward to the controller's discovery toggle so active MediaRouter
discovery only runs while the sheet is visible (battery cost).

Compose UI + NowPlaying wiring land next.
This commit is contained in:
2026-06-03 10:33:06 -04:00
parent 087486d253
commit 692d9dab60
@@ -0,0 +1,60 @@
package com.fabledsword.minstrel.player.output
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
/**
* NowPlaying-scoped projection over [OutputPickerController]. The
* controller is the long-lived Hilt singleton (its MediaRouter
* callback must not churn with screen lifecycle); this ViewModel is
* a thin lens over its [OutputPickerController.routesState] Flow
* plus the sheet's visibility state.
*
* Sheet open/close are forwarded to the controller's discovery
* toggle so active MediaRouter discovery only runs while the sheet
* is actually visible.
*/
@HiltViewModel
class OutputPickerViewModel @Inject constructor(
private val controller: OutputPickerController,
) : ViewModel() {
val routes: StateFlow<RouteSnapshot> = controller.routesState
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MS),
initialValue = controller.routesState.value,
)
private val sheetVisibleInternal = MutableStateFlow(false)
val sheetVisible: StateFlow<Boolean> = sheetVisibleInternal.asStateFlow()
fun onChipTapped() {
sheetVisibleInternal.value = true
controller.upgradeDiscovery()
}
fun onSheetDismissed() {
sheetVisibleInternal.value = false
controller.downgradeDiscovery()
}
fun onRouteSelected(route: OutputRoute) {
controller.select(route)
sheetVisibleInternal.value = false
controller.downgradeDiscovery()
}
private companion object {
// SharingStarted timeout so quick screen-orientation changes
// don't tear down + re-subscribe the controller's Flow.
const val STOP_TIMEOUT_MS = 5_000L
}
}