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

@ -5,7 +5,6 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.MutableLiveData
import androidx.work.WorkManager
import com.github.damontecres.wholphin.data.CurrentUser
import com.github.damontecres.wholphin.data.ServerRepository
@ -14,6 +13,7 @@ import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
@ -28,7 +28,7 @@ import org.junit.Test
class LatestNextUpSchedulerServiceTest {
@get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = StandardTestDispatcher()
private val currentLiveData = MutableLiveData<CurrentUser?>()
private val currentUser = MutableStateFlow<CurrentUser?>(null)
private val mockActivity = mockk<AppCompatActivity>(relaxed = true)
private val mockServerRepository = mockk<ServerRepository>(relaxed = true)
private val mockWorkManager = mockk<WorkManager>(relaxed = true)
@ -38,7 +38,7 @@ class LatestNextUpSchedulerServiceTest {
fun setUp() {
Dispatchers.setMain(testDispatcher)
every { mockActivity.lifecycle } returns lifecycleRegistry
every { mockServerRepository.current } returns currentLiveData
every { mockServerRepository.current } returns currentUser
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
@ -59,7 +59,7 @@ class LatestNextUpSchedulerServiceTest {
runTest {
createService()
currentLiveData.value = null
currentUser.value = null
advanceUntilIdle()
verify { mockWorkManager.cancelUniqueWork(LatestNextUpWorker.WORK_NAME) }

View file

@ -1,7 +1,6 @@
package com.github.damontecres.wholphin.services
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.MutableLiveData
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
@ -15,6 +14,7 @@ import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.StandardTestDispatcher
@ -98,8 +98,8 @@ class SuggestionServiceTest {
@Test
fun getSuggestionsFlow_returnsEmpty_whenNoUserLoggedIn() =
runTest {
val currentUser = MutableLiveData<JellyfinUser?>(null)
every { mockServerRepository.currentUser } returns currentUser
val currentUser = MutableStateFlow<JellyfinUser?>(null)
every { mockServerRepository.currentUserFlow } returns currentUser
val service = createService()
val result = service.getSuggestionsFlow(UUID.randomUUID(), BaseItemKind.MOVIE).first()
@ -113,10 +113,10 @@ class SuggestionServiceTest {
listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED).forEach { state ->
val userId = UUID.randomUUID()
val parentId = UUID.randomUUID()
val currentUser = MutableLiveData<JellyfinUser?>(mockUser(userId))
val currentUser = MutableStateFlow<JellyfinUser?>(mockUser(userId))
val workName = SuggestionsWorker.getOnDemandWorkName(userId, parentId, BaseItemKind.MOVIE)
every { mockServerRepository.currentUser } returns currentUser
every { mockServerRepository.currentUserFlow } returns currentUser
coEvery { mockCache.get(userId, parentId, BaseItemKind.MOVIE) } returns null
mockEnqueueOnDemandWork()
every { mockWorkManager.getWorkInfosForUniqueWorkFlow(workName) } returns
@ -142,10 +142,10 @@ class SuggestionServiceTest {
listOf(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED).forEach { state ->
val userId = UUID.randomUUID()
val parentId = UUID.randomUUID()
val currentUser = MutableLiveData<JellyfinUser?>(mockUser(userId))
val currentUser = MutableStateFlow<JellyfinUser?>(mockUser(userId))
val workName = SuggestionsWorker.getOnDemandWorkName(userId, parentId, BaseItemKind.MOVIE)
every { mockServerRepository.currentUser } returns currentUser
every { mockServerRepository.currentUserFlow } returns currentUser
coEvery { mockCache.get(userId, parentId, BaseItemKind.MOVIE) } returns null
mockEnqueueOnDemandWork()
every { mockWorkManager.getWorkInfosForUniqueWorkFlow(workName) } returns
@ -163,10 +163,10 @@ class SuggestionServiceTest {
runTest {
val userId = UUID.randomUUID()
val parentId = UUID.randomUUID()
val currentUser = MutableLiveData<JellyfinUser?>(mockUser(userId))
val currentUser = MutableStateFlow<JellyfinUser?>(mockUser(userId))
val workName = SuggestionsWorker.getOnDemandWorkName(userId, parentId, BaseItemKind.MOVIE)
every { mockServerRepository.currentUser } returns currentUser
every { mockServerRepository.currentUserFlow } returns currentUser
coEvery { mockCache.get(userId, parentId, BaseItemKind.MOVIE) } returns null
mockEnqueueOnDemandWork()
every { mockWorkManager.getWorkInfosForUniqueWorkFlow(workName) } returns flowOf(emptyList())
@ -189,11 +189,11 @@ class SuggestionServiceTest {
runTest {
val userId = UUID.randomUUID()
val parentId = UUID.randomUUID()
val currentUser = MutableLiveData<JellyfinUser?>(mockUser(userId))
val currentUser = MutableStateFlow<JellyfinUser?>(mockUser(userId))
val workName = SuggestionsWorker.getOnDemandWorkName(userId, parentId, BaseItemKind.MOVIE)
val cachedId = UUID.randomUUID()
every { mockServerRepository.currentUser } returns currentUser
every { mockServerRepository.currentUserFlow } returns currentUser
coEvery { mockCache.get(userId, parentId, BaseItemKind.MOVIE) } returnsMany
listOf(null, CachedSuggestions(listOf(cachedId)))
mockEnqueueOnDemandWork()
@ -220,9 +220,9 @@ class SuggestionServiceTest {
runTest {
val userId = UUID.randomUUID()
val parentId = UUID.randomUUID()
val currentUser = MutableLiveData<JellyfinUser?>(mockUser(userId))
val currentUser = MutableStateFlow<JellyfinUser?>(mockUser(userId))
every { mockServerRepository.currentUser } returns currentUser
every { mockServerRepository.currentUserFlow } returns currentUser
coEvery { mockCache.get(userId, parentId, BaseItemKind.MOVIE) } returns CachedSuggestions(emptyList())
val service = createService()
@ -244,10 +244,10 @@ class SuggestionServiceTest {
runTest {
val userId = UUID.randomUUID()
val parentId = UUID.randomUUID()
val currentUser = MutableLiveData<JellyfinUser?>(mockUser(userId))
val currentUser = MutableStateFlow<JellyfinUser?>(mockUser(userId))
val workName = SuggestionsWorker.getOnDemandWorkName(userId, parentId, BaseItemKind.MOVIE)
every { mockServerRepository.currentUser } returns currentUser
every { mockServerRepository.currentUserFlow } returns currentUser
coEvery { mockCache.get(userId, parentId, BaseItemKind.MOVIE) } returns null
mockEnqueueOnDemandWork()
every { mockWorkManager.getWorkInfosForUniqueWorkFlow(SuggestionsWorker.WORK_NAME) } returns
@ -268,10 +268,10 @@ class SuggestionServiceTest {
val userId = UUID.randomUUID()
val libraryId = UUID.randomUUID()
val otherLibraryId = UUID.randomUUID()
val currentUser = MutableLiveData<JellyfinUser?>(mockUser(userId))
val currentUser = MutableStateFlow<JellyfinUser?>(mockUser(userId))
val workName = SuggestionsWorker.getOnDemandWorkName(userId, libraryId, BaseItemKind.MOVIE)
every { mockServerRepository.currentUser } returns currentUser
every { mockServerRepository.currentUserFlow } returns currentUser
mockEnqueueOnDemandWork()
every { mockWorkManager.getWorkInfosForUniqueWorkFlow(workName) } returns flowOf(emptyList())
@ -304,9 +304,9 @@ class SuggestionServiceTest {
runTest {
val userId = UUID.randomUUID()
val parentId = UUID.randomUUID()
val currentUser = MutableLiveData<JellyfinUser?>(mockUser(userId))
val currentUser = MutableStateFlow<JellyfinUser?>(mockUser(userId))
every { mockServerRepository.currentUser } returns currentUser
every { mockServerRepository.currentUserFlow } returns currentUser
val cachedId = UUID.randomUUID()
coEvery {
@ -339,9 +339,9 @@ class SuggestionServiceTest {
runTest {
val userId = UUID.randomUUID()
val parentId = UUID.randomUUID()
val currentUser = MutableLiveData<JellyfinUser?>(mockUser(userId))
val currentUser = MutableStateFlow<JellyfinUser?>(mockUser(userId))
every { mockServerRepository.currentUser } returns currentUser
every { mockServerRepository.currentUserFlow } returns currentUser
val cachedId = UUID.randomUUID()
coEvery {

View file

@ -5,7 +5,6 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.MutableLiveData
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import com.github.damontecres.wholphin.data.CurrentUser
@ -19,6 +18,7 @@ import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
@ -35,7 +35,7 @@ import java.util.UUID
class SuggestionsSchedulerServiceTest {
@get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = StandardTestDispatcher()
private val currentLiveData = MutableLiveData<CurrentUser?>()
private val currentUser = MutableStateFlow<CurrentUser?>(null)
private val mockActivity = mockk<AppCompatActivity>(relaxed = true)
private val mockServerRepository = mockk<ServerRepository>(relaxed = true)
private val mockWorkManager = mockk<WorkManager>(relaxed = true)
@ -45,7 +45,7 @@ class SuggestionsSchedulerServiceTest {
fun setUp() {
Dispatchers.setMain(testDispatcher)
every { mockActivity.lifecycle } returns lifecycleRegistry
every { mockServerRepository.current } returns currentLiveData
every { mockServerRepository.current } returns currentUser
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
@ -57,8 +57,8 @@ class SuggestionsSchedulerServiceTest {
context = mockActivity,
serverRepository = mockServerRepository,
workManager = mockWorkManager,
dispatcher = testDispatcher,
).also {
it.dispatcher = testDispatcher
it.initialDelaySecondsProvider = { 60L }
}
@ -74,7 +74,7 @@ class SuggestionsSchedulerServiceTest {
runTest {
mockWorkInfos(emptyList())
createService()
currentLiveData.value =
currentUser.value =
CurrentUser(
user = JellyfinUser(id = UUID.randomUUID(), name = "User", serverId = UUID.randomUUID(), accessToken = "token"),
server = JellyfinServer(id = UUID.randomUUID(), name = "Server", url = "http://localhost", version = null),
@ -88,13 +88,13 @@ class SuggestionsSchedulerServiceTest {
runTest {
mockWorkInfos(emptyList())
createService()
currentLiveData.value =
currentUser.value =
CurrentUser(
user = JellyfinUser(id = UUID.randomUUID(), name = "User", serverId = UUID.randomUUID(), accessToken = "token"),
server = JellyfinServer(id = UUID.randomUUID(), name = "Server", url = "http://localhost", version = null),
)
advanceUntilIdle()
currentLiveData.value = null
currentUser.value = null
advanceUntilIdle()
verify { mockWorkManager.cancelUniqueWork(SuggestionsWorker.WORK_NAME) }
}
@ -113,7 +113,7 @@ class SuggestionsSchedulerServiceTest {
} returns mockk()
createService()
currentLiveData.value =
currentUser.value =
CurrentUser(
user = JellyfinUser(id = UUID.randomUUID(), name = "User", serverId = UUID.randomUUID(), accessToken = "token"),
server = JellyfinServer(id = UUID.randomUUID(), name = "Server", url = "http://localhost", version = null),
@ -138,7 +138,7 @@ class SuggestionsSchedulerServiceTest {
} returns mockk()
createService()
currentLiveData.value =
currentUser.value =
CurrentUser(
user = JellyfinUser(id = UUID.randomUUID(), name = "User", serverId = UUID.randomUUID(), accessToken = "token"),
server = JellyfinServer(id = UUID.randomUUID(), name = "Server", url = "http://localhost", version = null),
@ -159,7 +159,7 @@ class SuggestionsSchedulerServiceTest {
mockWorkInfos(listOf(workInfo))
createService()
currentLiveData.value =
currentUser.value =
CurrentUser(
user = JellyfinUser(id = userId, name = "User", serverId = UUID.randomUUID(), accessToken = "token"),
server = JellyfinServer(id = UUID.randomUUID(), name = "Server", url = "http://localhost", version = null),

View file

@ -1,7 +1,6 @@
package com.github.damontecres.wholphin.services
import androidx.datastore.core.DataStore
import androidx.lifecycle.MutableLiveData
import androidx.work.Data
import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
@ -15,6 +14,7 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkObject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.jellyfin.sdk.api.client.ApiClient
@ -112,7 +112,7 @@ class SuggestionsWorkerTest {
every { mockApi.accessToken } returns null
val mockUser = mockk<CurrentUser>()
var restored = false
every { mockServerRepository.current } answers { MutableLiveData(if (restored) mockUser else null) }
every { mockServerRepository.current } answers { MutableStateFlow(if (restored) mockUser else null) }
coEvery { mockServerRepository.restoreSession(testServerId, testUserId) } coAnswers {
restored = true
mockUser

View file

@ -1,6 +1,5 @@
package com.github.damontecres.wholphin.test
import androidx.lifecycle.MutableLiveData
import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.ItemPlayback
@ -773,20 +772,19 @@ private fun serverRepo(
): ServerRepository {
val mocked = mockk<ServerRepository>()
every { mocked.currentUserDto } returns
MutableLiveData(
UserDto(
id = UUID.randomUUID(),
hasPassword = true,
hasConfiguredPassword = true,
hasConfiguredEasyPassword = true,
configuration =
DefaultUserConfiguration.copy(
audioLanguagePreference = audioLang,
subtitleMode = subtitleMode ?: SubtitlePlaybackMode.DEFAULT,
subtitleLanguagePreference = subtitleLang,
),
),
UserDto(
id = UUID.randomUUID(),
hasPassword = true,
hasConfiguredPassword = true,
hasConfiguredEasyPassword = true,
configuration =
DefaultUserConfiguration.copy(
audioLanguagePreference = audioLang,
subtitleMode = subtitleMode ?: SubtitlePlaybackMode.DEFAULT,
subtitleLanguagePreference = subtitleLang,
),
)
return mocked
}

View file

@ -108,7 +108,7 @@ object TestModule {
.addInterceptor {
val request = it.request()
val newRequest =
serverRepository.currentUser.value?.accessToken?.let { token ->
serverRepository.currentUser?.accessToken?.let { token ->
request
.newBuilder()
.addHeader(