Remove dependency on livedata libraries (#1453)

## Description
Refactors all usages of `LiveData` into flows in keeping with best
practices. This also includes refactoring several `ViewModel`s to
combine multiple `LiveData` into a new single `StateFlow`.

There are no user facing changes

### Related issues
None

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-05-25 08:27:19 -04:00 • committed by GitHub
parent 982d9e6fe8
commit 0b450fc0ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
90 changed files with 2170 additions and 2283 deletions

View file

@ -2,10 +2,12 @@ package com.github.damontecres.wholphin.services
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope
import com.github.damontecres.wholphin.ui.collectLatestIn
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
import com.google.common.cache.CacheBuilder
import dagger.hilt.android.qualifiers.ActivityContext
@ -152,7 +154,7 @@ class DatePlayedInvalidationService
private val activity = (context as AppCompatActivity)
init {
serverRepository.current.observe(activity) {
serverRepository.current.collectLatestIn(activity.lifecycleScope) {
datePlayedService.invalidateAll()
}
}

View file

@ -265,8 +265,8 @@ class HomeSettingsService
*/
suspend fun createDefault(userId: UUID): HomePageResolvedSettings {
Timber.v("Creating default settings")
val user = serverRepository.currentUser.value?.takeIf { it.id == userId }
val userDto = serverRepository.currentUserDto.value?.takeIf { it.id == userId }
val user = serverRepository.currentUser?.takeIf { it.id == userId }
val userDto = serverRepository.currentUserDto?.takeIf { it.id == userId }
val libraries =
if (user != null) {
navDrawerService.getFilteredUserLibraries(user, userDto?.tvAccess ?: false)
@ -679,7 +679,7 @@ class HomeSettingsService
val genreImages =
getGenreImageMap(
api = api,
userId = serverRepository.currentUser.value?.id,
userId = serverRepository.currentUser?.id,
scope = scope,
imageUrlService = imageUrlService,
genres = genreIds,

View file

@ -14,6 +14,7 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.ui.collectLatestIn
import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -94,7 +95,7 @@ class LatestNextUpSchedulerService
internal var dispatcher: CoroutineDispatcher = Dispatchers.IO
init {
serverRepository.current.observe(activity) { user ->
serverRepository.current.collectLatestIn(activity.lifecycleScope) { user ->
Timber.v("New user %s", user?.user?.id)
if (user == null) {
workManager.cancelUniqueWork(LatestNextUpWorker.WORK_NAME)

View file

@ -63,7 +63,7 @@ class MediaManagementService
return enabled &&
item.canDelete &&
if (item.type == BaseItemKind.RECORDING) {
serverRepository.currentUserDto.value
serverRepository.currentUserDto
?.policy
?.enableLiveTvManagement == true
} else {

View file

@ -62,7 +62,7 @@ class MediaReportService
.content.mediaSources
val sourcesJson = json.encodeToString(sources)
val playbackPrefs = userPreferencesService.getCurrent().appPreferences.playbackPreferences
val serverVersion = serverRepository.currentServer.value?.serverVersion
val serverVersion = serverRepository.currentServer?.serverVersion
val deviceProfile =
deviceProfileService.getOrCreateDeviceProfile(playbackPrefs, serverVersion)
val deviceProfileJson = json.encodeToString(deviceProfile)

View file

@ -159,7 +159,7 @@ class MusicService
val items =
api.instantMixApi
.getInstantMixFromItem(
userId = serverRepository.currentUser.value?.id,
userId = serverRepository.currentUser?.id,
itemId = itemId,
limit = 200,
fields = DefaultItemFields,

View file

@ -1,7 +1,6 @@
package com.github.damontecres.wholphin.services
import android.content.Context
import androidx.lifecycle.asFlow
import com.github.damontecres.wholphin.data.ServerPreferencesDao
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinUser
@ -58,9 +57,8 @@ class NavDrawerService
init {
// Handle updating the nav drawer when the user changes
serverRepository.currentUser
.asFlow()
.combine(serverRepository.currentUserDto.asFlow()) { user, userDto ->
serverRepository.currentUserFlow
.combine(serverRepository.currentUserDtoFlow) { user, userDto ->
Pair(user, userDto)
}.onEach { (user, userDto) ->
Timber.d("User updated: user=%s, userDto=%s", user?.id, userDto?.id)

View file

@ -93,7 +93,7 @@ class PlaylistCreator
val request =
filter.applyTo(
GetItemsRequest(
userId = serverRepository.currentUser.value?.id,
userId = serverRepository.currentUser?.id,
parentId = playlistId,
fields = DefaultItemFields,
startIndex = startIndex,
@ -324,7 +324,7 @@ class PlaylistCreator
name: String,
initialItems: List<UUID>,
): UUID? =
serverRepository.currentUser.value?.let { user ->
serverRepository.currentUser?.let { user ->
api.playlistsApi
.createPlaylist(
CreatePlaylistDto(

View file

@ -94,7 +94,7 @@ class SeerrServerRepository
server = seerrServerDao.getServer(url)
}
server?.server?.let { server ->
serverRepository.currentUser.value?.let { jellyfinUser ->
serverRepository.currentUser?.let { jellyfinUser ->
// TODO test api key
val user =
SeerrUser(
@ -126,7 +126,7 @@ class SeerrServerRepository
server = seerrServerDao.getServer(url)
}
server?.server?.let { server ->
serverRepository.currentUser.value?.let { jellyfinUser ->
serverRepository.currentUser?.let { jellyfinUser ->
// TODO Need to update server early so that cookies are saved
seerrApi.update(server.url, null)
val userConfig = seerrLogin(seerrApi.api, authMethod, username, password)

View file

@ -9,6 +9,7 @@ import androidx.lifecycle.lifecycleScope
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.ui.collectLatestIn
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.showToast
import dagger.hilt.android.qualifiers.ActivityContext
@ -42,7 +43,7 @@ class ServerEventListener
init {
activity.lifecycle.addObserver(this)
serverRepository.current.observe(activity) {
serverRepository.current.collectLatestIn(activity.lifecycleScope) {
Timber.d("New user/server: %s", it)
listenJob?.cancel()
if (it != null) {

View file

@ -30,7 +30,7 @@ class StreamChoiceService
private val serverRepository: ServerRepository,
private val playbackLanguageChoiceDao: PlaybackLanguageChoiceDao,
) {
private val userConfig: UserConfiguration? get() = serverRepository.currentUserDto.value?.configuration
private val userConfig: UserConfiguration? get() = serverRepository.currentUserDto?.configuration
suspend fun updateAudio(
dto: BaseItemDto,
@ -58,7 +58,7 @@ class StreamChoiceService
) {
val seriesId = dto.seriesId
if (seriesId != null) {
val userId = serverRepository.currentUser.value!!.rowId
val userId = serverRepository.currentUser!!.rowId
val currentPlc =
playbackLanguageChoiceDao.get(userId, seriesId)
?: PlaybackLanguageChoice(userId, seriesId, dto.id)
@ -70,7 +70,7 @@ class StreamChoiceService
suspend fun getPlaybackLanguageChoice(dto: BaseItemDto) =
dto.seriesId?.let {
playbackLanguageChoiceDao.get(serverRepository.currentUser.value!!.rowId, it)
playbackLanguageChoiceDao.get(serverRepository.currentUser!!.rowId, it)
}
/**
@ -107,7 +107,13 @@ class StreamChoiceService
plc: PlaybackLanguageChoice?,
prefs: UserPreferences,
): MediaStream? {
val plc = plc ?: seriesId?.let { playbackLanguageChoiceDao.get(serverRepository.currentUser.value!!.rowId, it) }
val plc =
plc ?: seriesId?.let {
playbackLanguageChoiceDao.get(
serverRepository.currentUser!!.rowId,
it,
)
}
return source.mediaStreams?.letNotEmpty { streams ->
val candidates = streams.filter { it.type == MediaStreamType.AUDIO }
chooseAudioStream(candidates, itemPlayback, plc, prefs)
@ -158,7 +164,7 @@ class StreamChoiceService
val plc =
plc ?: seriesId?.let {
playbackLanguageChoiceDao.get(
serverRepository.currentUser.value!!.rowId,
serverRepository.currentUser!!.rowId,
it,
)
}
@ -194,7 +200,7 @@ class StreamChoiceService
}
val itemPlayback =
ItemPlayback(
userId = serverRepository.currentUser.value!!.rowId,
userId = serverRepository.currentUser!!.rowId,
itemId = UUID.randomUUID(), // Not used for ONLY_FORCED resolution
subtitleIndex = TrackIndex.ONLY_FORCED,
)

View file

@ -1,6 +1,5 @@
package com.github.damontecres.wholphin.services
import androidx.lifecycle.asFlow
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
@ -52,8 +51,7 @@ class SuggestionService
parentId: UUID,
itemKind: BaseItemKind,
): Flow<SuggestionsResource> {
return serverRepository.currentUser
.asFlow()
return serverRepository.currentUserFlow
.flatMapLatest { user ->
val userId = user?.id ?: return@flatMapLatest flowOf(SuggestionsResource.Empty)
val cachedSuggestions = cache.get(userId, parentId, itemKind)

View file

@ -8,15 +8,14 @@ import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.services.hilt.IoDispatcher
import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -35,6 +34,7 @@ class SuggestionsSchedulerService
@param:ActivityContext private val context: Context,
private val serverRepository: ServerRepository,
private val workManager: WorkManager,
@param:IoDispatcher private val dispatcher: CoroutineDispatcher,
) {
private val activity =
(context as? AppCompatActivity)
@ -42,18 +42,18 @@ class SuggestionsSchedulerService
"SuggestionsSchedulerService requires an AppCompatActivity context, but received: ${context::class.java.name}",
)
// Exposed for testing
internal var dispatcher: CoroutineDispatcher = Dispatchers.IO
internal var initialDelaySecondsProvider: () -> Long = { 60L + Random.nextLong(0L, 121L) }
init {
serverRepository.current.observe(activity) { user ->
Timber.v("New user %s", user?.user?.id)
if (user == null) {
workManager.cancelUniqueWork(SuggestionsWorker.WORK_NAME)
} else {
activity.lifecycleScope.launch(dispatcher + ExceptionHandler()) {
scheduleWork(user.user.id, user.server.id)
activity.lifecycleScope.launch(dispatcher + ExceptionHandler()) {
serverRepository.current.collect { user ->
Timber.v("New user %s", user?.user?.id)
if (user == null) {
workManager.cancelUniqueWork(SuggestionsWorker.WORK_NAME)
} else {
activity.lifecycleScope.launch(dispatcher + ExceptionHandler()) {
scheduleWork(user.user.id, user.server.id)
}
}
}
}

View file

@ -22,7 +22,7 @@ class UserPreferencesService
val flow = preferencesDataStore.data.map { UserPreferences(it) }
suspend fun getCurrent(): UserPreferences =
serverRepository.currentUserDto.value!!.configuration.let { userConfig ->
serverRepository.currentUserDto!!.configuration.let { userConfig ->
val appPrefs = preferencesDataStore.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
UserPreferences(
appPrefs,

View file

@ -4,7 +4,6 @@ import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.asFlow
import androidx.lifecycle.lifecycleScope
import com.github.damontecres.wholphin.BuildConfig
import com.github.damontecres.wholphin.data.SeerrServerDao
@ -39,7 +38,7 @@ class UserSwitchListener
init {
context as AppCompatActivity
context.lifecycleScope.launchDefault {
serverRepository.currentUser.asFlow().collect { user ->
serverRepository.currentUserFlow.collect { user ->
Timber.d("New user")
seerrServerRepository.clear()
homeSettingsService.currentSettings.update { HomePageResolvedSettings.EMPTY }

View file

@ -116,7 +116,7 @@ object AppModule {
.addInterceptor {
val request = it.request()
val newRequest =
serverRepository.currentUser.value?.accessToken?.let { token ->
serverRepository.current.value?.user?.accessToken?.let { token ->
request
.newBuilder()
.addHeader(
@ -170,7 +170,10 @@ object AppModule {
appPreference: DataStore<AppPreferences>,
@IoCoroutineScope scope: CoroutineScope,
) = object : RememberTabManager {
fun key(itemId: String) = "${serverRepository.currentServer.value?.id}_${serverRepository.currentUser.value?.id}_$itemId"
fun key(itemId: String): String =
serverRepository.current.value.let {
"${it?.server?.id}_${it?.user?.id}_$itemId"
}
override fun getRememberedTab(
preferences: UserPreferences,

View file

@ -13,6 +13,7 @@ import androidx.work.WorkManager
import androidx.work.await
import androidx.work.workDataOf
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.ui.collectLatestIn
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.qualifiers.ActivityContext
@ -43,7 +44,7 @@ class TvProviderSchedulerService
context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
init {
serverRepository.current.observe(activity) { user ->
serverRepository.current.collectLatestIn(activity.lifecycleScope) { user ->
workManager.cancelUniqueWork(TvProviderWorker.WORK_NAME)
if (supportsTvProvider) {
if (user != null) {