From 0caa5a13e58fcda7da06620415d34ed0ac334f7e Mon Sep 17 00:00:00 2001 From: Paul Klauser Date: Tue, 11 Jun 2024 16:51:17 -0400 Subject: [PATCH 1/2] Reactively collect phrases for a category --- .../vocable/presets/PresetsViewModel.kt | 44 ++++--- .../willowtree/vocable/FakePhrasesUseCase.kt | 3 +- .../vocable/presets/PresetsViewModelTest.kt | 108 +++++++----------- 3 files changed, 64 insertions(+), 91 deletions(-) diff --git a/app/src/main/java/com/willowtree/vocable/presets/PresetsViewModel.kt b/app/src/main/java/com/willowtree/vocable/presets/PresetsViewModel.kt index edbccd83..2070deb1 100644 --- a/app/src/main/java/com/willowtree/vocable/presets/PresetsViewModel.kt +++ b/app/src/main/java/com/willowtree/vocable/presets/PresetsViewModel.kt @@ -12,7 +12,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -29,20 +31,20 @@ class PresetsViewModel( .asLiveData() // Will only ever be null immediately on init - private val liveSelectedCategoryId = MutableStateFlow(null) + private val selectedCategoryId = MutableStateFlow(null) val selectedCategory: StateFlow = combine( categoriesUseCase.categories(), - liveSelectedCategoryId + selectedCategoryId ) { categories, selectedId -> val currentCategory = categories.find { it.categoryId == selectedId } if (currentCategory?.hidden == true) { val newSortOrder = (currentCategory.sortOrder + 1) - val newCategory = categories.find { it.sortOrder == newSortOrder} + val newCategory = categories.find { it.sortOrder == newSortOrder } if (newCategory != null) { - liveSelectedCategoryId.update { newCategory.categoryId } - categories.find { it.sortOrder == newSortOrder} + selectedCategoryId.update { newCategory.categoryId } + categories.find { it.sortOrder == newSortOrder } } else { - liveSelectedCategoryId.update { categories.first().categoryId } + selectedCategoryId.update { categories.first().categoryId } categories.first() } } else { @@ -51,29 +53,25 @@ class PresetsViewModel( }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), null) val selectedCategoryLiveData: LiveData = selectedCategory.asLiveData() - val currentPhrases: LiveData> = liveSelectedCategoryId - .map(::mapCategoryIdToPhrases) - .asLiveData() - - private suspend fun mapCategoryIdToPhrases(categoryId: String?): List { - if (categoryId == null) return emptyList() - return idlingResourceContainer.run { - val phrases: MutableList = phrasesUseCase.getPhrasesForCategory(categoryId) - .run { + val currentPhrases: LiveData> = selectedCategoryId + .filterNotNull() + .flatMapLatest { categoryId -> + phrasesUseCase.getPhrasesForCategoryFlow(categoryId).map { phrases -> + val phrasesToReturn = phrases.run { if (categoryId != PresetCategories.RECENTS.id) { sortedBy { it.sortOrder } } else { this } + }.toMutableList() + if (categoryId != PresetCategories.RECENTS.id && categoryId != PresetCategories.USER_KEYPAD.id && phrases.isNotEmpty()) { + //Add null to end of normal non empty category phrase list for the "+ Add Phrase" button + phrasesToReturn.add(null) } - .toMutableList() - //Add null to end of normal non empty category phrase list for the "+ Add Phrase" button - if (categoryId != PresetCategories.RECENTS.id && categoryId != PresetCategories.USER_KEYPAD.id && phrases.isNotEmpty()) { - phrases.add(null) + phrasesToReturn } - phrases } - } + .asLiveData() private val liveNavToAddPhrase = MutableLiveData() val navToAddPhrase: LiveData = liveNavToAddPhrase @@ -81,7 +79,7 @@ class PresetsViewModel( init { viewModelScope.launch { idlingResourceContainer.run { - liveSelectedCategoryId.update { + selectedCategoryId.update { categoriesUseCase.categories().first().first().categoryId } } @@ -89,7 +87,7 @@ class PresetsViewModel( } fun onCategorySelected(categoryId: String) { - liveSelectedCategoryId.update { categoryId } + selectedCategoryId.update { categoryId } } fun addToRecents(phrase: Phrase) { diff --git a/app/src/test/java/com/willowtree/vocable/FakePhrasesUseCase.kt b/app/src/test/java/com/willowtree/vocable/FakePhrasesUseCase.kt index abe180ce..476ec728 100644 --- a/app/src/test/java/com/willowtree/vocable/FakePhrasesUseCase.kt +++ b/app/src/test/java/com/willowtree/vocable/FakePhrasesUseCase.kt @@ -7,6 +7,7 @@ import com.willowtree.vocable.room.PhraseDto import com.willowtree.vocable.utils.locale.LocalesWithText import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf class FakePhrasesUseCase : IPhrasesUseCase { @@ -41,7 +42,7 @@ class FakePhrasesUseCase : IPhrasesUseCase { } override fun getPhrasesForCategoryFlow(categoryId: String): Flow> { - TODO("Not yet implemented") + return flowOf(_categoriesToPhrases[categoryId]!!.map { it.asPhrase() }) } override suspend fun updatePhraseLastSpokenTime(phraseId: String) { diff --git a/app/src/test/java/com/willowtree/vocable/presets/PresetsViewModelTest.kt b/app/src/test/java/com/willowtree/vocable/presets/PresetsViewModelTest.kt index c44a7832..676d670c 100644 --- a/app/src/test/java/com/willowtree/vocable/presets/PresetsViewModelTest.kt +++ b/app/src/test/java/com/willowtree/vocable/presets/PresetsViewModelTest.kt @@ -1,6 +1,7 @@ package com.willowtree.vocable.presets import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import app.cash.turbine.test import com.willowtree.vocable.FakeCategoriesUseCase import com.willowtree.vocable.FakePhrasesUseCase import com.willowtree.vocable.MainDispatcherRule @@ -11,7 +12,6 @@ import com.willowtree.vocable.utils.IdlingResourceContainerImpl import com.willowtree.vocable.utils.locale.LocalesWithText import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -94,24 +94,17 @@ class PresetsViewModelTest { val vm = createViewModel() vm.onCategorySelected("1") - //TODO: PK - Turbine may make this less painful, punting for now - var category: Category? = null - val job = launch { - vm.selectedCategory.collect { - category = it - } + vm.selectedCategory.test { + assertEquals( + Category.StoredCategory( + categoryId = "1", + localizedName = LocalesWithText(mapOf("en_US" to "category")), + hidden = false, + sortOrder = 0 + ), + awaitItem() + ) } - job.cancel() - - assertEquals( - Category.StoredCategory( - categoryId = "1", - localizedName = LocalesWithText(mapOf("en_US" to "category")), - hidden = false, - sortOrder = 0 - ), - category - ) } @OptIn(ExperimentalCoroutinesApi::class) @@ -138,24 +131,17 @@ class PresetsViewModelTest { vm.onCategorySelected("1") - var category: Category? = null - val job = launch { - vm.selectedCategory.collect { - category = it - } + vm.selectedCategory.test { + assertEquals( + Category.StoredCategory( + categoryId = "2", + localizedName = LocalesWithText(mapOf("en_US" to "second category")), + hidden = false, + sortOrder = 1 + ), + awaitItem() + ) } - job.cancel() - - assertEquals( - Category.StoredCategory( - categoryId = "2", - localizedName = LocalesWithText(mapOf("en_US" to "second category")), - hidden = false, - sortOrder = 1 - ), - category - ) - } @OptIn(ExperimentalCoroutinesApi::class) @@ -188,23 +174,17 @@ class PresetsViewModelTest { vm.onCategorySelected("3") - var category: Category? = null - val job = launch { - vm.selectedCategory.collect { - category = it - } + vm.selectedCategory.test { + assertEquals( + Category.StoredCategory( + categoryId = "1", + localizedName = LocalesWithText(mapOf("en_US" to "category")), + hidden = false, + sortOrder = 0 + ), + awaitItem() + ) } - job.cancel() - - assertEquals( - Category.StoredCategory( - categoryId = "1", - localizedName = LocalesWithText(mapOf("en_US" to "category")), - hidden = false, - sortOrder = 0 - ), - category - ) } @OptIn(ExperimentalCoroutinesApi::class) @@ -237,23 +217,17 @@ class PresetsViewModelTest { vm.onCategorySelected("3") - var category: Category? = null - val job = launch { - vm.selectedCategory.collect { - category = it - } + vm.selectedCategory.test { + assertEquals( + Category.StoredCategory( + categoryId = "2", + localizedName = LocalesWithText(mapOf("en_US" to "second category")), + hidden = false, + sortOrder = 1 + ), + awaitItem() + ) } - job.cancel() - - assertEquals( - Category.StoredCategory( - categoryId = "2", - localizedName = LocalesWithText(mapOf("en_US" to "second category")), - hidden = false, - sortOrder = 1 - ), - category - ) } @Test From 8e7269225253d3c2b9f0406b7fcce36a167069ca Mon Sep 17 00:00:00 2001 From: Paul Klauser Date: Tue, 11 Jun 2024 17:01:47 -0400 Subject: [PATCH 2/2] Remove duplicate dao methods and funnel usage to the Flows --- .../com/willowtree/vocable/room/MigrationTest.kt | 5 +++-- .../java/com/willowtree/vocable/PhrasesUseCase.kt | 9 ++------- .../presets/LegacyCategoriesAndPhrasesRepository.kt | 5 +++-- .../java/com/willowtree/vocable/room/PhraseDao.kt | 10 ++-------- .../vocable/room/RoomStoredPhrasesRepository.kt | 12 ++---------- .../vocable/room/StoredPhrasesRepository.kt | 2 -- .../willowtree/vocable/settings/SettingsViewModel.kt | 11 ----------- 7 files changed, 12 insertions(+), 42 deletions(-) delete mode 100644 app/src/main/java/com/willowtree/vocable/settings/SettingsViewModel.kt diff --git a/app/src/androidTest/java/com/willowtree/vocable/room/MigrationTest.kt b/app/src/androidTest/java/com/willowtree/vocable/room/MigrationTest.kt index d53b295a..6bab7088 100644 --- a/app/src/androidTest/java/com/willowtree/vocable/room/MigrationTest.kt +++ b/app/src/androidTest/java/com/willowtree/vocable/room/MigrationTest.kt @@ -17,6 +17,7 @@ import com.willowtree.vocable.presets.PresetCategories import com.willowtree.vocable.utility.VocableKoinTestRule import com.willowtree.vocable.utils.VocableSharedPreferences import com.willowtree.vocable.utils.locale.LocalesWithText +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Assert.assertEquals @@ -341,8 +342,8 @@ class MigrationTest { ), categories ) - val customPhrases = db.phraseDao().getPhrasesForCategory("custom") - val recentPhrases = db.phraseDao().getPhrasesForCategory("recents") + val customPhrases = db.phraseDao().getPhrasesForCategory("custom").first() + val recentPhrases = db.phraseDao().getPhrasesForCategory("recents").first() assertEquals( listOf( PhraseDto( diff --git a/app/src/main/java/com/willowtree/vocable/PhrasesUseCase.kt b/app/src/main/java/com/willowtree/vocable/PhrasesUseCase.kt index 39c189ee..f93d0339 100644 --- a/app/src/main/java/com/willowtree/vocable/PhrasesUseCase.kt +++ b/app/src/main/java/com/willowtree/vocable/PhrasesUseCase.kt @@ -14,6 +14,7 @@ import com.willowtree.vocable.utils.locale.LocaleProvider import com.willowtree.vocable.utils.locale.LocalesWithText import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first class PhrasesUseCase( private val legacyPhrasesRepository: ILegacyCategoriesAndPhrasesRepository, @@ -24,13 +25,7 @@ class PhrasesUseCase( private val localeProvider: LocaleProvider ) : IPhrasesUseCase { override suspend fun getPhrasesForCategory(categoryId: String): List { - if (categoryId == PresetCategories.RECENTS.id) { - val presets = presetPhrasesRepository.getRecentPhrases() - val stored = storedPhrasesRepository.getRecentPhrases() - return (presets + stored).sortedByDescending { it.lastSpokenDate }.take(8) - } - return storedPhrasesRepository.getPhrasesForCategory(categoryId) + - presetPhrasesRepository.getPhrasesForCategory(categoryId) + return getPhrasesForCategoryFlow(categoryId).first() } override fun getPhrasesForCategoryFlow(categoryId: String): Flow> { diff --git a/app/src/main/java/com/willowtree/vocable/presets/LegacyCategoriesAndPhrasesRepository.kt b/app/src/main/java/com/willowtree/vocable/presets/LegacyCategoriesAndPhrasesRepository.kt index d31c26c3..ef755aab 100644 --- a/app/src/main/java/com/willowtree/vocable/presets/LegacyCategoriesAndPhrasesRepository.kt +++ b/app/src/main/java/com/willowtree/vocable/presets/LegacyCategoriesAndPhrasesRepository.kt @@ -8,6 +8,7 @@ import com.willowtree.vocable.room.PhraseDto import com.willowtree.vocable.room.VocableDatabase import com.willowtree.vocable.utils.locale.LocalesWithText import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import org.koin.core.component.KoinComponent class LegacyCategoriesAndPhrasesRepository( @@ -24,11 +25,11 @@ class LegacyCategoriesAndPhrasesRepository( } override suspend fun getPhrasesForCategory(categoryId: String): List { - return database.phraseDao().getPhrasesForCategory(categoryId) + return database.phraseDao().getPhrasesForCategory(categoryId).first() } override suspend fun getRecentPhrases(): List = - database.phraseDao().getRecentPhrases() + database.phraseDao().getRecentPhrases().first() override suspend fun updateCategorySortOrders(categorySortOrders: List) { database.categoryDao().updateCategorySortOrders(categorySortOrders) diff --git a/app/src/main/java/com/willowtree/vocable/room/PhraseDao.kt b/app/src/main/java/com/willowtree/vocable/room/PhraseDao.kt index 11be3b63..ce7c8c84 100644 --- a/app/src/main/java/com/willowtree/vocable/room/PhraseDao.kt +++ b/app/src/main/java/com/willowtree/vocable/room/PhraseDao.kt @@ -30,16 +30,10 @@ interface PhraseDao { suspend fun updatePhraseLocalizedUtterance(phraseLocalizedUtterance: PhraseLocalizedUtterance) @Query("SELECT * FROM Phrase WHERE last_spoken_date IS NOT NULL ORDER BY last_spoken_date DESC LIMIT 8") - suspend fun getRecentPhrases(): List - - @Query("SELECT * FROM Phrase WHERE last_spoken_date IS NOT NULL ORDER BY last_spoken_date DESC LIMIT 8") - fun getRecentPhrasesFlow(): Flow> - - @Query("SELECT * FROM Phrase WHERE parent_category_id == :categoryId") - suspend fun getPhrasesForCategory(categoryId: String): List + fun getRecentPhrases(): Flow> @Query("SELECT * FROM Phrase WHERE parent_category_id == :categoryId") - fun getPhrasesForCategoryFlow(categoryId: String): Flow> + fun getPhrasesForCategory(categoryId: String): Flow> @Query("SELECT * FROM Phrase WHERE phrase_id == :phraseId") suspend fun getPhrase(phraseId: String): PhraseDto? diff --git a/app/src/main/java/com/willowtree/vocable/room/RoomStoredPhrasesRepository.kt b/app/src/main/java/com/willowtree/vocable/room/RoomStoredPhrasesRepository.kt index ff5628fb..2523c3d4 100644 --- a/app/src/main/java/com/willowtree/vocable/room/RoomStoredPhrasesRepository.kt +++ b/app/src/main/java/com/willowtree/vocable/room/RoomStoredPhrasesRepository.kt @@ -24,21 +24,13 @@ class RoomStoredPhrasesRepository( ) } - override suspend fun getRecentPhrases(): List { - return database.phraseDao().getRecentPhrases().map { it.asPhrase() } - } - override fun getRecentPhrasesFlow(): Flow> { - return database.phraseDao().getRecentPhrasesFlow() + return database.phraseDao().getRecentPhrases() .map { phraseList -> phraseList.map { it.asPhrase() } } } - override suspend fun getPhrasesForCategory(categoryId: String): List { - return database.phraseDao().getPhrasesForCategory(categoryId).map { it.asPhrase() } - } - override fun getPhrasesForCategoryFlow(categoryId: String): Flow> { - return database.phraseDao().getPhrasesForCategoryFlow(categoryId) + return database.phraseDao().getPhrasesForCategory(categoryId) .map { phraseList -> phraseList.map { it.asPhrase() } } } diff --git a/app/src/main/java/com/willowtree/vocable/room/StoredPhrasesRepository.kt b/app/src/main/java/com/willowtree/vocable/room/StoredPhrasesRepository.kt index 66c27f43..205b3172 100644 --- a/app/src/main/java/com/willowtree/vocable/room/StoredPhrasesRepository.kt +++ b/app/src/main/java/com/willowtree/vocable/room/StoredPhrasesRepository.kt @@ -7,9 +7,7 @@ import kotlinx.coroutines.flow.Flow interface StoredPhrasesRepository { suspend fun addPhrase(phrase: PhraseDto) suspend fun updatePhraseLastSpokenTime(phraseId: String) - suspend fun getRecentPhrases(): List fun getRecentPhrasesFlow(): Flow> - suspend fun getPhrasesForCategory(categoryId: String): List fun getPhrasesForCategoryFlow(categoryId: String): Flow> suspend fun getPhrase(phraseId: String): Phrase? suspend fun updatePhrase(phrase: PhraseDto) diff --git a/app/src/main/java/com/willowtree/vocable/settings/SettingsViewModel.kt b/app/src/main/java/com/willowtree/vocable/settings/SettingsViewModel.kt deleted file mode 100644 index 5c180bc0..00000000 --- a/app/src/main/java/com/willowtree/vocable/settings/SettingsViewModel.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.willowtree.vocable.settings - -import androidx.lifecycle.ViewModel -import com.willowtree.vocable.presets.LegacyCategoriesAndPhrasesRepository -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -class SettingsViewModel : ViewModel(), KoinComponent { - - private val presetsRepository: LegacyCategoriesAndPhrasesRepository by inject() -}