mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-16 10:56:50 -04:00
Merge pull request #4322 from kiwix/Fixes#4308
Refactored the `RxJava` to coroutines in `PageViewModel`.
This commit is contained in:
commit
307e7249de
@ -112,7 +112,7 @@ class KiwixRoomDatabaseTest {
|
||||
|
||||
// test inserting into history database
|
||||
historyRoomDao.saveHistory(historyItem)
|
||||
var historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
var historyList = historyRoomDao.historyRoomEntity().first()
|
||||
with(historyList.first()) {
|
||||
assertThat(historyTitle, equalTo(historyItem.title))
|
||||
assertThat(zimId, equalTo(historyItem.zimId))
|
||||
@ -126,7 +126,7 @@ class KiwixRoomDatabaseTest {
|
||||
|
||||
// test deleting the history
|
||||
historyRoomDao.deleteHistory(listOf(historyItem))
|
||||
historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
historyList = historyRoomDao.historyRoomEntity().first()
|
||||
assertEquals(historyList.size, 0)
|
||||
|
||||
// test deleting all history
|
||||
@ -134,10 +134,10 @@ class KiwixRoomDatabaseTest {
|
||||
historyRoomDao.saveHistory(
|
||||
getHistoryItem(databaseId = 2)
|
||||
)
|
||||
historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
historyList = historyRoomDao.historyRoomEntity().first()
|
||||
assertEquals(historyList.size, 2)
|
||||
historyRoomDao.deleteAllHistory()
|
||||
historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
historyList = historyRoomDao.historyRoomEntity().first()
|
||||
assertEquals(historyList.size, 0)
|
||||
}
|
||||
|
||||
@ -146,7 +146,7 @@ class KiwixRoomDatabaseTest {
|
||||
runBlocking {
|
||||
notesRoomDao = db.notesRoomDao()
|
||||
// delete all the notes from database to properly run the test cases.
|
||||
notesRoomDao.deleteNotes(notesRoomDao.notes().blockingFirst() as List<NoteListItem>)
|
||||
notesRoomDao.deleteNotes(notesRoomDao.notes().first() as List<NoteListItem>)
|
||||
val noteItem =
|
||||
getNoteListItem(
|
||||
zimUrl = "http://kiwix.app/MainPage",
|
||||
@ -155,7 +155,7 @@ class KiwixRoomDatabaseTest {
|
||||
|
||||
// Save and retrieve a notes item
|
||||
notesRoomDao.saveNote(noteItem)
|
||||
var notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
var notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
with(notesList.first()) {
|
||||
assertThat(zimId, equalTo(noteItem.zimId))
|
||||
assertThat(zimUrl, equalTo(noteItem.zimUrl))
|
||||
@ -168,7 +168,7 @@ class KiwixRoomDatabaseTest {
|
||||
|
||||
// test deleting the history
|
||||
notesRoomDao.deleteNotes(listOf(noteItem))
|
||||
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
assertEquals(notesList.size, 0)
|
||||
|
||||
// test deleting all notes
|
||||
@ -179,10 +179,10 @@ class KiwixRoomDatabaseTest {
|
||||
zimUrl = "http://kiwix.app/Installing"
|
||||
)
|
||||
)
|
||||
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
assertEquals(notesList.size, 2)
|
||||
notesRoomDao.deletePages(notesRoomDao.notes().blockingFirst())
|
||||
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
notesRoomDao.deletePages(notesRoomDao.notes().first())
|
||||
notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
assertEquals(notesList.size, 0)
|
||||
}
|
||||
|
||||
|
@ -215,7 +215,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
kiwixRoomDatabase.recentSearchRoomDao().deleteSearchHistory()
|
||||
kiwixRoomDatabase.historyRoomDao().deleteAllHistory()
|
||||
kiwixRoomDatabase.notesRoomDao()
|
||||
.deletePages(kiwixRoomDatabase.notesRoomDao().notes().blockingFirst())
|
||||
.deletePages(kiwixRoomDatabase.notesRoomDao().notes().first())
|
||||
box.removeAll()
|
||||
}
|
||||
|
||||
@ -238,7 +238,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
// migrate data into room database
|
||||
objectBoxToRoomMigrator.migrateHistory(box)
|
||||
// check if data successfully migrated to room
|
||||
val actual = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst()
|
||||
val actual = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
|
||||
with(actual.first()) {
|
||||
assertThat(historyTitle, equalTo(historyItem.title))
|
||||
assertThat(zimId, equalTo(historyItem.zimId))
|
||||
@ -254,7 +254,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
|
||||
// Migrate data from empty ObjectBox database
|
||||
objectBoxToRoomMigrator.migrateHistory(box)
|
||||
var actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst()
|
||||
var actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
|
||||
assertTrue(actualData.isEmpty())
|
||||
|
||||
// Test if data successfully migrated to Room and existing data is preserved
|
||||
@ -262,7 +262,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
box.put(HistoryEntity(historyItem2))
|
||||
// Migrate data into Room database
|
||||
objectBoxToRoomMigrator.migrateHistory(box)
|
||||
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst()
|
||||
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
|
||||
assertEquals(2, actualData.size)
|
||||
val existingItem =
|
||||
actualData.find {
|
||||
@ -281,7 +281,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
kiwixRoomDatabase.historyRoomDao().saveHistory(historyItem)
|
||||
box.put(HistoryEntity(historyItem))
|
||||
objectBoxToRoomMigrator.migrateHistory(box)
|
||||
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst()
|
||||
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
|
||||
assertEquals(1, actualData.size)
|
||||
|
||||
clearRoomAndBoxStoreDatabases(box)
|
||||
@ -296,7 +296,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
kiwixRoomDatabase.historyRoomDao().saveHistory(historyItem4)
|
||||
box.put(HistoryEntity(historyItem))
|
||||
objectBoxToRoomMigrator.migrateHistory(box)
|
||||
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst()
|
||||
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
|
||||
assertEquals(2, actualData.size)
|
||||
|
||||
clearRoomAndBoxStoreDatabases(box)
|
||||
@ -310,7 +310,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
// Ensure Room database remains empty or unaffected by the invalid data
|
||||
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst()
|
||||
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
|
||||
assertTrue(actualData.isEmpty())
|
||||
|
||||
// Test large data migration for recent searches
|
||||
@ -332,7 +332,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
val endTime = System.currentTimeMillis()
|
||||
val migrationTime = endTime - startTime
|
||||
// Check if data successfully migrated to Room
|
||||
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst()
|
||||
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
|
||||
assertEquals(numEntities, actualData.size)
|
||||
// Assert that the migration completes within a reasonable time frame
|
||||
assertTrue(
|
||||
@ -367,7 +367,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
// migrate data into room database
|
||||
objectBoxToRoomMigrator.migrateNotes(box)
|
||||
// check if data successfully migrated to room
|
||||
var notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List<NoteListItem>
|
||||
var notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List<NoteListItem>
|
||||
with(notesList.first()) {
|
||||
assertThat(zimId, equalTo(noteItem.zimId))
|
||||
assertThat(zimUrl, equalTo(noteItem.zimUrl))
|
||||
@ -382,7 +382,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
|
||||
// Migrate data from empty ObjectBox database
|
||||
objectBoxToRoomMigrator.migrateNotes(box)
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List<NoteListItem>
|
||||
assertTrue(notesList.isEmpty())
|
||||
|
||||
// Test if data successfully migrated to Room and existing data is preserved
|
||||
@ -390,7 +390,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
box.put(NotesEntity(noteItem))
|
||||
// Migrate data into Room database
|
||||
objectBoxToRoomMigrator.migrateNotes(box)
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List<NoteListItem>
|
||||
assertEquals(noteItem.title, notesList.first().title)
|
||||
assertEquals(2, notesList.size)
|
||||
val existingItem =
|
||||
@ -411,7 +411,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
box.put(NotesEntity(noteItem1))
|
||||
// Migrate data into Room database
|
||||
objectBoxToRoomMigrator.migrateNotes(box)
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List<NoteListItem>
|
||||
assertEquals(1, notesList.size)
|
||||
|
||||
clearRoomAndBoxStoreDatabases(box)
|
||||
@ -426,7 +426,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
kiwixRoomDatabase.notesRoomDao().saveNote(noteItem1)
|
||||
box.put(NotesEntity(noteItem2))
|
||||
objectBoxToRoomMigrator.migrateNotes(box)
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List<NoteListItem>
|
||||
assertEquals(2, notesList.size)
|
||||
|
||||
clearRoomAndBoxStoreDatabases(box)
|
||||
@ -440,7 +440,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
// Ensure Room database remains empty or unaffected by the invalid data
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List<NoteListItem>
|
||||
assertTrue(notesList.isEmpty())
|
||||
|
||||
// Test large data migration for recent searches
|
||||
@ -462,7 +462,7 @@ class ObjectBoxToRoomMigratorTest {
|
||||
val endTime = System.currentTimeMillis()
|
||||
val migrationTime = endTime - startTime
|
||||
// Check if data successfully migrated to Room
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List<NoteListItem>
|
||||
assertEquals(numEntities, notesList.size)
|
||||
// Assert that the migration completes within a reasonable time frame
|
||||
assertTrue(
|
||||
|
@ -22,6 +22,7 @@ import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.core.IsEqual.equalTo
|
||||
@ -66,7 +67,7 @@ class HistoryRoomDaoTest {
|
||||
|
||||
// Save and retrieve a history item
|
||||
historyRoomDao.saveHistory(historyItem)
|
||||
var historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
var historyList = historyRoomDao.historyRoomEntity().first()
|
||||
with(historyList.first()) {
|
||||
assertThat(historyTitle, equalTo(historyItem.title))
|
||||
assertThat(zimId, equalTo(historyItem.zimId))
|
||||
@ -80,26 +81,26 @@ class HistoryRoomDaoTest {
|
||||
|
||||
// Test to update the same day history for url
|
||||
historyRoomDao.saveHistory(historyItem)
|
||||
historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
historyList = historyRoomDao.historyRoomEntity().first()
|
||||
assertEquals(historyList.size, 1)
|
||||
|
||||
// Delete the saved history item
|
||||
historyRoomDao.deleteHistory(listOf(historyItem))
|
||||
historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
historyList = historyRoomDao.historyRoomEntity().first()
|
||||
assertEquals(historyList.size, 0)
|
||||
|
||||
// Save and delete all history items
|
||||
historyRoomDao.saveHistory(historyItem)
|
||||
historyRoomDao.saveHistory(getHistoryItem(databaseId = 2, dateString = "31 May 2024"))
|
||||
historyRoomDao.deleteAllHistory()
|
||||
historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
historyList = historyRoomDao.historyRoomEntity().first()
|
||||
assertThat(historyList.size, equalTo(0))
|
||||
|
||||
// Save history item with empty fields
|
||||
val emptyHistoryUrl = ""
|
||||
val emptyTitle = ""
|
||||
historyRoomDao.saveHistory(getHistoryItem(emptyTitle, emptyHistoryUrl, databaseId = 1))
|
||||
historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
historyList = historyRoomDao.historyRoomEntity().first()
|
||||
assertThat(historyList.size, equalTo(1))
|
||||
historyRoomDao.deleteAllHistory()
|
||||
|
||||
@ -113,14 +114,14 @@ class HistoryRoomDaoTest {
|
||||
dateString = "31 May 2024"
|
||||
)
|
||||
historyRoomDao.saveHistory(historyItem1)
|
||||
historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
historyList = historyRoomDao.historyRoomEntity().first()
|
||||
assertThat(historyList.size, equalTo(2))
|
||||
historyRoomDao.deleteAllHistory()
|
||||
|
||||
// Save two entity with same and database id with same date to see if it's updated or not.
|
||||
historyRoomDao.saveHistory(historyItem)
|
||||
historyRoomDao.saveHistory(historyItem)
|
||||
historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
historyList = historyRoomDao.historyRoomEntity().first()
|
||||
assertThat(historyList.size, equalTo(1))
|
||||
historyRoomDao.deleteAllHistory()
|
||||
|
||||
@ -132,7 +133,7 @@ class HistoryRoomDaoTest {
|
||||
"Undefined value was saved into database",
|
||||
false
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
assertThat("Undefined value was not saved, as expected.", true)
|
||||
}
|
||||
|
||||
@ -140,13 +141,13 @@ class HistoryRoomDaoTest {
|
||||
val unicodeTitle = "title \u03A3" // Unicode character for Greek capital letter Sigma
|
||||
val historyItem2 = getHistoryItem(title = unicodeTitle, databaseId = 2)
|
||||
historyRoomDao.saveHistory(historyItem2)
|
||||
historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
historyList = historyRoomDao.historyRoomEntity().first()
|
||||
assertThat(historyList.first().historyTitle, equalTo("title Σ"))
|
||||
|
||||
// Test deletePages function
|
||||
historyRoomDao.saveHistory(historyItem)
|
||||
historyRoomDao.deletePages(listOf(historyItem, historyItem2))
|
||||
historyList = historyRoomDao.historyRoomEntity().blockingFirst()
|
||||
historyList = historyRoomDao.historyRoomEntity().first()
|
||||
assertThat(historyList.size, equalTo(0))
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.core.IsEqual.equalTo
|
||||
@ -65,7 +66,7 @@ class NoteRoomDaoTest {
|
||||
|
||||
// Save and retrieve a notes item
|
||||
notesRoomDao.saveNote(noteItem)
|
||||
var notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
var notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
with(notesList.first()) {
|
||||
assertThat(zimId, equalTo(noteItem.zimId))
|
||||
assertThat(zimUrl, equalTo(noteItem.zimUrl))
|
||||
@ -78,25 +79,25 @@ class NoteRoomDaoTest {
|
||||
|
||||
// Test update the existing note
|
||||
notesRoomDao.saveNote(noteItem)
|
||||
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
assertEquals(notesList.size, 1)
|
||||
|
||||
// Delete the saved note item with all delete methods available in NoteRoomDao.
|
||||
// delete via noteTitle
|
||||
notesRoomDao.deleteNote(noteItem.title)
|
||||
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
assertEquals(notesList.size, 0)
|
||||
|
||||
// delete with deletePages method
|
||||
notesRoomDao.saveNote(noteItem)
|
||||
notesRoomDao.deletePages(listOf(noteItem))
|
||||
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
assertEquals(notesList.size, 0)
|
||||
|
||||
// delete with list of NoteListItem
|
||||
notesRoomDao.saveNote(noteItem)
|
||||
notesRoomDao.deleteNotes(listOf(noteItem))
|
||||
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
assertEquals(notesList.size, 0)
|
||||
|
||||
// Save note with empty title
|
||||
@ -107,7 +108,7 @@ class NoteRoomDaoTest {
|
||||
noteFilePath = "/storage/emulated/0/Download/Notes/Alpine linux/Installing.txt"
|
||||
)
|
||||
)
|
||||
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
assertEquals(notesList.size, 1)
|
||||
clearNotes()
|
||||
|
||||
@ -127,7 +128,7 @@ class NoteRoomDaoTest {
|
||||
)
|
||||
kiwixRoomDatabase.notesRoomDao().saveNote(noteItem2)
|
||||
kiwixRoomDatabase.notesRoomDao().saveNote(noteItem3)
|
||||
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
assertEquals(2, notesList.size)
|
||||
clearNotes()
|
||||
|
||||
@ -139,7 +140,7 @@ class NoteRoomDaoTest {
|
||||
"Undefined value was saved into database",
|
||||
false
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
assertThat("Undefined value was not saved, as expected.", true)
|
||||
}
|
||||
|
||||
@ -148,11 +149,11 @@ class NoteRoomDaoTest {
|
||||
val noteListItem2 =
|
||||
getNoteListItem(title = unicodeTitle, zimUrl = "http://kiwix.app/Installing")
|
||||
notesRoomDao.saveNote(noteListItem2)
|
||||
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem>
|
||||
notesList = notesRoomDao.notes().first() as List<NoteListItem>
|
||||
assertThat(notesList.first().title, equalTo("title Σ"))
|
||||
}
|
||||
|
||||
private suspend fun clearNotes() {
|
||||
notesRoomDao.deleteNotes(notesRoomDao.notes().blockingFirst() as List<NoteListItem>)
|
||||
notesRoomDao.deleteNotes(notesRoomDao.notes().first() as List<NoteListItem>)
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import org.kiwix.kiwixmobile.cachedComponent
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
||||
@ -55,7 +54,6 @@ class LanguageFragment : BaseFragment() {
|
||||
|
||||
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
private var composeView: ComposeView? = null
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
override fun inject(baseActivity: BaseActivity) {
|
||||
baseActivity.cachedComponent.inject(this)
|
||||
@ -153,7 +151,6 @@ class LanguageFragment : BaseFragment() {
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
compositeDisposable.clear()
|
||||
composeView?.disposeComposition()
|
||||
composeView = null
|
||||
}
|
||||
|
@ -25,8 +25,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
|
||||
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
|
||||
@ -55,21 +56,17 @@ class LanguageViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun observeActions() =
|
||||
viewModelScope.launch {
|
||||
actions
|
||||
.map { action -> reduce(action, state.value) }
|
||||
.distinctUntilChanged()
|
||||
.collect { newState -> state.value = newState }
|
||||
}
|
||||
actions
|
||||
.map { action -> reduce(action, state.value) }
|
||||
.distinctUntilChanged()
|
||||
.onEach { newState -> state.value = newState }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
private fun observeLanguages() =
|
||||
viewModelScope.launch {
|
||||
languageDao.languages()
|
||||
.filter { it.isNotEmpty() }
|
||||
.collect { languages ->
|
||||
actions.tryEmit(UpdateLanguages(languages))
|
||||
}
|
||||
}
|
||||
languageDao.languages()
|
||||
.filter { it.isNotEmpty() }
|
||||
.onEach { languages -> actions.tryEmit(UpdateLanguages(languages)) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
override fun onCleared() {
|
||||
coroutineJobs.forEach {
|
||||
|
@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.core.base
|
||||
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import org.kiwix.kiwixmobile.core.base.BaseContract.Presenter
|
||||
import org.kiwix.kiwixmobile.core.base.BaseContract.View
|
||||
|
||||
@ -26,9 +25,6 @@ import org.kiwix.kiwixmobile.core.base.BaseContract.View
|
||||
*/
|
||||
@Suppress("UnnecessaryAbstractClass")
|
||||
abstract class BasePresenter<T : View<*>?> : Presenter<T> {
|
||||
@JvmField
|
||||
val compositeDisposable = CompositeDisposable()
|
||||
|
||||
@JvmField
|
||||
var view: T? = null
|
||||
|
||||
@ -38,6 +34,5 @@ abstract class BasePresenter<T : View<*>?> : Presenter<T> {
|
||||
|
||||
override fun detachView() {
|
||||
view = null
|
||||
compositeDisposable.clear()
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ package org.kiwix.kiwixmobile.core.dao
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.kotlin.query
|
||||
import io.objectbox.query.QueryBuilder
|
||||
import io.reactivex.Flowable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity_
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.Page
|
||||
@ -29,8 +30,8 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseV
|
||||
import javax.inject.Inject
|
||||
|
||||
class HistoryDao @Inject constructor(val box: Box<HistoryEntity>) : PageDao {
|
||||
fun history(): Flowable<List<Page>> =
|
||||
box.asFlowable(
|
||||
fun history(): Flow<List<Page>> =
|
||||
box.asFlow(
|
||||
box.query {
|
||||
orderDesc(HistoryEntity_.timeStamp)
|
||||
}
|
||||
@ -46,7 +47,7 @@ class HistoryDao @Inject constructor(val box: Box<HistoryEntity>) : PageDao {
|
||||
}
|
||||
}
|
||||
|
||||
override fun pages(): Flowable<List<Page>> = history()
|
||||
override fun pages(): Flow<List<Page>> = history()
|
||||
override fun deletePages(pagesToDelete: List<Page>) =
|
||||
deleteHistory(pagesToDelete as List<HistoryItem>)
|
||||
|
||||
|
@ -24,7 +24,8 @@ import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.Update
|
||||
import io.reactivex.Flowable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.HistoryRoomEntity
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.Page
|
||||
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem
|
||||
@ -33,9 +34,9 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
|
||||
@Dao
|
||||
abstract class HistoryRoomDao : PageDao {
|
||||
@Query("SELECT * FROM HistoryRoomEntity ORDER BY HistoryRoomEntity.timeStamp DESC")
|
||||
abstract fun historyRoomEntity(): Flowable<List<HistoryRoomEntity>>
|
||||
abstract fun historyRoomEntity(): Flow<List<HistoryRoomEntity>>
|
||||
|
||||
fun history(): Flowable<List<Page>> =
|
||||
fun history(): Flow<List<Page>> =
|
||||
historyRoomEntity().map {
|
||||
it.map { historyEntity ->
|
||||
historyEntity.zimFilePath?.let { filePath ->
|
||||
|
@ -21,7 +21,6 @@ package org.kiwix.kiwixmobile.core.dao
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.util.Base64
|
||||
import io.reactivex.Flowable
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -30,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.reactive.asPublisher
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.kiwix.kiwixmobile.core.CoreApp
|
||||
@ -121,10 +119,7 @@ class LibkiwixBookmarks @Inject constructor(
|
||||
bookmarkListFlow
|
||||
.map { it }
|
||||
|
||||
// Currently kept in RxJava Flowable because `PageViewModel` still expects RxJava streams.
|
||||
// This can be refactored to use Kotlin Flow once `PageViewModel` is migrated to coroutines.
|
||||
override fun pages(): Flowable<List<Page>> =
|
||||
Flowable.fromPublisher(bookmarks().asPublisher())
|
||||
override fun pages(): Flow<List<Page>> = bookmarks()
|
||||
|
||||
override fun deletePages(pagesToDelete: List<Page>) =
|
||||
deleteBookmarks(pagesToDelete as List<LibkiwixBookmarkItem>)
|
||||
|
@ -20,8 +20,11 @@ package org.kiwix.kiwixmobile.core.dao
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.kotlin.query
|
||||
import io.objectbox.query.QueryBuilder
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.BookmarkEntity
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.BookmarkEntity_
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.Page
|
||||
@ -31,8 +34,8 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseV
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewBookmarksDao @Inject constructor(val box: Box<BookmarkEntity>) : PageDao {
|
||||
fun bookmarks(): Flowable<List<Page>> =
|
||||
box.asFlowable(
|
||||
fun bookmarks(): Flow<List<Page>> =
|
||||
box.asFlow(
|
||||
box.query {
|
||||
order(BookmarkEntity_.bookmarkTitle)
|
||||
}
|
||||
@ -48,7 +51,7 @@ class NewBookmarksDao @Inject constructor(val box: Box<BookmarkEntity>) : PageDa
|
||||
}
|
||||
}
|
||||
|
||||
override fun pages(): Flowable<List<Page>> = bookmarks()
|
||||
override fun pages(): Flow<List<Page>> = bookmarks()
|
||||
override fun deletePages(pagesToDelete: List<Page>) =
|
||||
deleteBookmarks(pagesToDelete as List<BookmarkItem>)
|
||||
|
||||
@ -71,8 +74,11 @@ class NewBookmarksDao @Inject constructor(val box: Box<BookmarkEntity>) : PageDa
|
||||
.toList()
|
||||
.distinct()
|
||||
|
||||
fun bookmarkUrlsForCurrentBook(zimFileReader: ZimFileReader?): Flowable<List<String>> =
|
||||
box.asFlowable(
|
||||
fun bookmarkUrlsForCurrentBook(
|
||||
zimFileReader: ZimFileReader?,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
): Flow<List<String>> =
|
||||
box.asFlow(
|
||||
box.query {
|
||||
equal(
|
||||
BookmarkEntity_.zimId,
|
||||
@ -88,7 +94,7 @@ class NewBookmarksDao @Inject constructor(val box: Box<BookmarkEntity>) : PageDa
|
||||
order(BookmarkEntity_.bookmarkTitle)
|
||||
}
|
||||
).map { it.map(BookmarkEntity::bookmarkUrl) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flowOn(dispatcher)
|
||||
|
||||
fun saveBookmark(bookmarkItem: BookmarkItem) {
|
||||
box.put(BookmarkEntity(bookmarkItem))
|
||||
|
@ -21,9 +21,6 @@ import io.objectbox.Box
|
||||
import io.objectbox.kotlin.flow
|
||||
import io.objectbox.kotlin.query
|
||||
import io.objectbox.query.Query
|
||||
import io.objectbox.rx.RxQuery
|
||||
import io.reactivex.BackpressureStrategy
|
||||
import io.reactivex.BackpressureStrategy.LATEST
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@ -53,9 +50,3 @@ fun <T> Box<T>.asFlow(query: Query<T> = query {}): Flow<List<T>> {
|
||||
.map { it.toList() }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
internal fun <T> Box<T>.asFlowable(
|
||||
query: Query<T> = query {},
|
||||
backpressureStrategy: BackpressureStrategy = LATEST
|
||||
) =
|
||||
RxQuery.observable(query).toFlowable(backpressureStrategy)
|
||||
|
@ -21,7 +21,8 @@ package org.kiwix.kiwixmobile.core.dao
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.kotlin.query
|
||||
import io.objectbox.query.QueryBuilder
|
||||
import io.reactivex.Flowable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity_
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.Page
|
||||
@ -30,8 +31,8 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseV
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewNoteDao @Inject constructor(val box: Box<NotesEntity>) : PageDao {
|
||||
fun notes(): Flowable<List<Page>> =
|
||||
box.asFlowable(
|
||||
fun notes(): Flow<List<Page>> =
|
||||
box.asFlow(
|
||||
box.query {
|
||||
order(NotesEntity_.noteTitle)
|
||||
}
|
||||
@ -47,7 +48,7 @@ class NewNoteDao @Inject constructor(val box: Box<NotesEntity>) : PageDao {
|
||||
}
|
||||
}
|
||||
|
||||
override fun pages(): Flowable<List<Page>> = notes()
|
||||
override fun pages(): Flow<List<Page>> = notes()
|
||||
|
||||
override fun deletePages(pagesToDelete: List<Page>) =
|
||||
deleteNotes(pagesToDelete as List<NoteListItem>)
|
||||
|
@ -22,10 +22,11 @@ import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import io.reactivex.Flowable
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.NotesRoomEntity
|
||||
import org.kiwix.kiwixmobile.core.extensions.deleteFile
|
||||
@ -38,9 +39,9 @@ import java.io.File
|
||||
@Dao
|
||||
abstract class NotesRoomDao : PageDao {
|
||||
@Query("SELECT * FROM NotesRoomEntity ORDER BY NotesRoomEntity.noteTitle")
|
||||
abstract fun notesAsEntity(): Flowable<List<NotesRoomEntity>>
|
||||
abstract fun notesAsEntity(): Flow<List<NotesRoomEntity>>
|
||||
|
||||
fun notes(): Flowable<List<Page>> =
|
||||
fun notes(): Flow<List<Page>> =
|
||||
notesAsEntity().map {
|
||||
it.map { notesEntity ->
|
||||
notesEntity.zimFilePath?.let { filePath ->
|
||||
@ -53,7 +54,7 @@ abstract class NotesRoomDao : PageDao {
|
||||
}
|
||||
}
|
||||
|
||||
override fun pages(): Flowable<List<Page>> = notes()
|
||||
override fun pages(): Flow<List<Page>> = notes()
|
||||
override fun deletePages(pagesToDelete: List<Page>) =
|
||||
deleteNotes(pagesToDelete as List<NoteListItem>)
|
||||
|
||||
|
@ -18,10 +18,10 @@
|
||||
|
||||
package org.kiwix.kiwixmobile.core.dao
|
||||
|
||||
import io.reactivex.Flowable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.Page
|
||||
|
||||
interface PageDao {
|
||||
fun pages(): Flowable<List<Page>>
|
||||
fun pages(): Flow<List<Page>>
|
||||
fun deletePages(pagesToDelete: List<Page>)
|
||||
}
|
||||
|
@ -32,13 +32,16 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.referentialEqualityPolicy
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.base.BaseFragment
|
||||
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
|
||||
import org.kiwix.kiwixmobile.core.extensions.update
|
||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.OnItemClickListener
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.Page
|
||||
@ -66,7 +69,7 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
|
||||
|
||||
@Inject lateinit var alertDialogShower: AlertDialogShower
|
||||
private var actionMode: ActionMode? = null
|
||||
val compositeDisposable = CompositeDisposable()
|
||||
private val coroutineJobs = mutableListOf<Job>()
|
||||
abstract val screenTitle: Int
|
||||
abstract val noItemsString: String
|
||||
abstract val switchString: String
|
||||
@ -116,36 +119,51 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.menu_context_delete) {
|
||||
pageViewModel.actions.offer(Action.UserClickedDeleteSelectedPages)
|
||||
pageViewModel.actions.tryEmit(Action.UserClickedDeleteSelectedPages)
|
||||
return true
|
||||
}
|
||||
pageViewModel.actions.offer(Action.ExitActionModeMenu)
|
||||
pageViewModel.actions.tryEmit(Action.ExitActionModeMenu)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
pageViewModel.actions.offer(Action.ExitActionModeMenu)
|
||||
pageViewModel.actions.tryEmit(Action.ExitActionModeMenu)
|
||||
actionMode = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
pageScreenState.value = pageScreenState.value.copy(
|
||||
searchQueryHint = searchQueryHint,
|
||||
searchText = "",
|
||||
searchValueChangedListener = { onTextChanged(it) },
|
||||
clearSearchButtonClickListener = { onTextChanged("") },
|
||||
screenTitle = screenTitle,
|
||||
noItemsString = noItemsString,
|
||||
switchString = switchString,
|
||||
switchIsChecked = switchIsChecked,
|
||||
onSwitchCheckedChanged = { onSwitchCheckedChanged(it).invoke() },
|
||||
deleteIconTitle = deleteIconTitle
|
||||
)
|
||||
pageScreenState.update {
|
||||
copy(
|
||||
searchQueryHint = this@PageFragment.searchQueryHint,
|
||||
searchText = "",
|
||||
searchValueChangedListener = { onTextChanged(it) },
|
||||
clearSearchButtonClickListener = { onTextChanged("") },
|
||||
screenTitle = this@PageFragment.screenTitle,
|
||||
noItemsString = this@PageFragment.noItemsString,
|
||||
switchString = this@PageFragment.switchString,
|
||||
switchIsChecked = this@PageFragment.switchIsChecked,
|
||||
onSwitchCheckedChanged = { onSwitchChanged(it).invoke() },
|
||||
deleteIconTitle = this@PageFragment.deleteIconTitle
|
||||
)
|
||||
}
|
||||
val activity = requireActivity() as CoreMainActivity
|
||||
compositeDisposable.add(pageViewModel.effects.subscribe { it.invokeWith(activity) })
|
||||
pageViewModel.state.observe(viewLifecycleOwner, Observer(::render))
|
||||
cancelCoroutineJobs()
|
||||
coroutineJobs.apply {
|
||||
add(
|
||||
pageViewModel
|
||||
.effects
|
||||
.onEach { it.invokeWith(activity) }
|
||||
.launchIn(lifecycleScope)
|
||||
)
|
||||
add(
|
||||
pageViewModel
|
||||
.state
|
||||
.onEach { render(it) }
|
||||
.launchIn(lifecycleScope)
|
||||
)
|
||||
}
|
||||
pageViewModel.alertDialogShower = alertDialogShower
|
||||
}
|
||||
|
||||
@ -168,9 +186,9 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
|
||||
isSearchActive = pageScreenState.value.isSearchActive,
|
||||
onSearchClick = {
|
||||
// Set the `isSearchActive` when the search button is clicked.
|
||||
pageScreenState.value = pageScreenState.value.copy(isSearchActive = true)
|
||||
pageScreenState.update { copy(isSearchActive = true) }
|
||||
},
|
||||
onDeleteClick = { pageViewModel.actions.offer(Action.UserClickedDeleteButton) }
|
||||
onDeleteClick = { pageViewModel.actions.tryEmit(Action.UserClickedDeleteButton) }
|
||||
)
|
||||
)
|
||||
DialogHost(alertDialogShower)
|
||||
@ -186,8 +204,8 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
|
||||
* @param searchText The current text entered in the search bar.
|
||||
*/
|
||||
private fun onTextChanged(searchText: String) {
|
||||
pageScreenState.value = pageScreenState.value.copy(searchText = searchText)
|
||||
pageViewModel.actions.offer(Action.Filter(searchText))
|
||||
pageScreenState.update { copy(searchText = searchText) }
|
||||
pageViewModel.actions.tryEmit(Action.Filter(searchText))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -197,9 +215,9 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
|
||||
*
|
||||
* @param isChecked The new checked state of the switch.
|
||||
*/
|
||||
private fun onSwitchCheckedChanged(isChecked: Boolean): () -> Unit = {
|
||||
pageScreenState.value = pageScreenState.value.copy(switchIsChecked = isChecked)
|
||||
pageViewModel.actions.offer(Action.UserClickedShowAllToggle(isChecked))
|
||||
private fun onSwitchChanged(isChecked: Boolean): () -> Unit = {
|
||||
pageScreenState.update { copy(switchIsChecked = isChecked) }
|
||||
pageViewModel.actions.tryEmit(Action.UserClickedShowAllToggle(isChecked))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -209,7 +227,7 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
|
||||
*/
|
||||
private fun navigationIconClick(): () -> Unit = {
|
||||
if (pageScreenState.value.isSearchActive) {
|
||||
pageScreenState.value = pageScreenState.value.copy(isSearchActive = false)
|
||||
pageScreenState.update { copy(isSearchActive = false) }
|
||||
onTextChanged("")
|
||||
} else {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
@ -255,21 +273,20 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
compositeDisposable.clear()
|
||||
cancelCoroutineJobs()
|
||||
}
|
||||
|
||||
private fun cancelCoroutineJobs() {
|
||||
coroutineJobs.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
coroutineJobs.clear()
|
||||
}
|
||||
|
||||
private fun render(state: PageState<*>) {
|
||||
pageScreenState.value = pageScreenState.value.copy(
|
||||
switchIsEnabled = !state.isInSelectionState,
|
||||
// First, assign the existing state to force Compose to recognize a change.
|
||||
// This helps when internal properties of items (like `isSelected`) change,
|
||||
// but the list reference itself remains the same — Compose won't detect it otherwise.
|
||||
pageState = pageState.value
|
||||
)
|
||||
// Then, assign the actual updated state to trigger full recomposition.
|
||||
pageScreenState.value = pageScreenState.value.copy(
|
||||
pageState = state
|
||||
)
|
||||
pageScreenState.update {
|
||||
copy(switchIsEnabled = !state.isInSelectionState, pageState = state)
|
||||
}
|
||||
if (state.isInSelectionState) {
|
||||
if (actionMode == null) {
|
||||
actionMode =
|
||||
@ -282,9 +299,9 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
|
||||
}
|
||||
|
||||
override fun onItemClick(page: Page) {
|
||||
pageViewModel.actions.offer(Action.OnItemClick(page))
|
||||
pageViewModel.actions.tryEmit(Action.OnItemClick(page))
|
||||
}
|
||||
|
||||
override fun onItemLongClick(page: Page): Boolean =
|
||||
pageViewModel.actions.offer(Action.OnItemLongClick(page))
|
||||
pageViewModel.actions.tryEmit(Action.OnItemLongClick(page))
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ class BookmarkViewModel @Inject constructor(
|
||||
action: Action.UserClickedShowAllToggle,
|
||||
state: BookmarkState
|
||||
): BookmarkState {
|
||||
effects.offer(UpdateAllBookmarksPreference(sharedPreferenceUtil, action.isChecked))
|
||||
effects.tryEmit(UpdateAllBookmarksPreference(sharedPreferenceUtil, action.isChecked))
|
||||
return state.copy(showAll = action.isChecked)
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,8 @@ package org.kiwix.kiwixmobile.core.page.bookmark.viewmodel.effects
|
||||
*/
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.dao.PageDao
|
||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent
|
||||
@ -32,7 +32,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteAllBookmarks
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedBookmarks
|
||||
|
||||
data class ShowDeleteBookmarksDialog(
|
||||
private val effects: PublishProcessor<SideEffect<*>>,
|
||||
private val effects: MutableSharedFlow<SideEffect<*>>,
|
||||
private val state: PageState<LibkiwixBookmarkItem>,
|
||||
private val pageDao: PageDao,
|
||||
private val viewModelScope: CoroutineScope,
|
||||
@ -42,7 +42,7 @@ data class ShowDeleteBookmarksDialog(
|
||||
activity.cachedComponent.inject(this)
|
||||
dialogShower.show(
|
||||
if (state.isInSelectionState) DeleteSelectedBookmarks else DeleteAllBookmarks,
|
||||
{ effects.offer(DeletePageItems(state, pageDao, viewModelScope)) }
|
||||
{ effects.tryEmit(DeletePageItems(state, pageDao, viewModelScope)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ class HistoryViewModel @Inject constructor(
|
||||
action: Action.UserClickedShowAllToggle,
|
||||
state: HistoryState
|
||||
): HistoryState {
|
||||
effects.offer(UpdateAllHistoryPreference(sharedPreferenceUtil, action.isChecked))
|
||||
effects.tryEmit(UpdateAllHistoryPreference(sharedPreferenceUtil, action.isChecked))
|
||||
return state.copy(showAll = action.isChecked)
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,8 @@ package org.kiwix.kiwixmobile.core.page.history.viewmodel.effects
|
||||
*/
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.dao.PageDao
|
||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent
|
||||
@ -31,7 +31,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteAllHistory
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedHistory
|
||||
|
||||
data class ShowDeleteHistoryDialog(
|
||||
private val effects: PublishProcessor<SideEffect<*>>,
|
||||
private val effects: MutableSharedFlow<SideEffect<*>>,
|
||||
private val state: HistoryState,
|
||||
private val pageDao: PageDao,
|
||||
private val viewModelScope: CoroutineScope,
|
||||
@ -40,7 +40,7 @@ data class ShowDeleteHistoryDialog(
|
||||
override fun invokeWith(activity: AppCompatActivity) {
|
||||
activity.cachedComponent.inject(this)
|
||||
dialogShower.show(if (state.isInSelectionState) DeleteSelectedHistory else DeleteAllHistory, {
|
||||
effects.offer(DeletePageItems(state, pageDao, viewModelScope))
|
||||
effects.tryEmit(DeletePageItems(state, pageDao, viewModelScope))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ class NotesViewModel @Inject constructor(
|
||||
action: Action.UserClickedShowAllToggle,
|
||||
state: NotesState
|
||||
): NotesState {
|
||||
effects.offer(UpdateAllNotesPreference(sharedPreferenceUtil, action.isChecked))
|
||||
effects.tryEmit(UpdateAllNotesPreference(sharedPreferenceUtil, action.isChecked))
|
||||
return state.copy(showAll = action.isChecked)
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,8 @@ package org.kiwix.kiwixmobile.core.page.notes.viewmodel.effects
|
||||
*/
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.dao.PageDao
|
||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent
|
||||
@ -31,7 +31,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteAllNotes
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedNotes
|
||||
|
||||
data class ShowDeleteNotesDialog(
|
||||
private val effects: PublishProcessor<SideEffect<*>>,
|
||||
private val effects: MutableSharedFlow<SideEffect<*>>,
|
||||
private val state: NotesState,
|
||||
private val pageDao: PageDao,
|
||||
private val viewModelScope: CoroutineScope,
|
||||
@ -42,7 +42,7 @@ data class ShowDeleteNotesDialog(
|
||||
dialogShower.show(
|
||||
if (state.isInSelectionState) DeleteSelectedNotes else DeleteAllNotes,
|
||||
{
|
||||
effects.offer(DeletePageItems(state, pageDao, viewModelScope))
|
||||
effects.tryEmit(DeletePageItems(state, pageDao, viewModelScope))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
package org.kiwix.kiwixmobile.core.page.notes.viewmodel.effects
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.Page
|
||||
@ -31,7 +31,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.ShowNoteDialog
|
||||
|
||||
data class ShowOpenNoteDialog(
|
||||
private val effects: PublishProcessor<SideEffect<*>>,
|
||||
private val effects: MutableSharedFlow<SideEffect<*>>,
|
||||
private val page: Page,
|
||||
private val zimReaderContainer: ZimReaderContainer,
|
||||
private val dialogShower: DialogShower
|
||||
@ -40,10 +40,10 @@ data class ShowOpenNoteDialog(
|
||||
activity.cachedComponent.inject(this)
|
||||
dialogShower.show(
|
||||
ShowNoteDialog,
|
||||
{ effects.offer(OpenPage(page, zimReaderContainer)) },
|
||||
{ effects.tryEmit(OpenPage(page, zimReaderContainer)) },
|
||||
{
|
||||
val item = page as NoteListItem
|
||||
effects.offer(OpenNote(item))
|
||||
effects.tryEmit(OpenNote(item))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ package org.kiwix.kiwixmobile.core.page.viewmodel
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.Page
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.PageRelated
|
||||
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
|
||||
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem
|
||||
import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem
|
||||
|
||||
abstract class PageState<T : Page> {
|
||||
abstract val pageItems: List<T>
|
||||
@ -43,8 +45,12 @@ abstract class PageState<T : Page> {
|
||||
val currentItemIdentifier = if (it is LibkiwixBookmarkItem) it.url else it.id
|
||||
val pageIdentifier = if (it is LibkiwixBookmarkItem) page.url else page.id
|
||||
if (currentItemIdentifier == pageIdentifier) {
|
||||
it.apply {
|
||||
isSelected = !isSelected
|
||||
when (it) {
|
||||
is LibkiwixBookmarkItem -> it.copy(isSelected = !it.isSelected) as T
|
||||
is HistoryItem -> it.copy(isSelected = !it.isSelected) as T
|
||||
is NoteListItem -> it.copy(isSelected = !it.isSelected) as T
|
||||
// For test cases only.
|
||||
else -> it.apply { isSelected = !isSelected }
|
||||
}
|
||||
} else {
|
||||
it
|
||||
|
@ -18,14 +18,21 @@
|
||||
|
||||
package org.kiwix.kiwixmobile.core.page.viewmodel
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.dao.PageDao
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.Page
|
||||
@ -54,32 +61,32 @@ abstract class PageViewModel<T : Page, S : PageState<T>>(
|
||||
lateinit var alertDialogShower: AlertDialogShower
|
||||
|
||||
private lateinit var pageViewModelClickListener: PageViewModelClickListener
|
||||
private val _state = MutableStateFlow(initialState())
|
||||
val state: StateFlow<S> = _state.asStateFlow()
|
||||
val effects = MutableSharedFlow<SideEffect<*>>(extraBufferCapacity = Int.MAX_VALUE)
|
||||
val actions = MutableSharedFlow<Action>(extraBufferCapacity = Int.MAX_VALUE)
|
||||
private val coroutineJobs = mutableListOf<Job>()
|
||||
|
||||
val state: MutableLiveData<S> by lazy {
|
||||
MutableLiveData<S>().apply {
|
||||
value = initialState()
|
||||
@VisibleForTesting
|
||||
fun getMutableStateForTestCases() = _state
|
||||
|
||||
init {
|
||||
coroutineJobs.apply {
|
||||
add(observeActions())
|
||||
add(observePages())
|
||||
}
|
||||
}
|
||||
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
val effects = PublishProcessor.create<SideEffect<*>>()
|
||||
val actions = PublishProcessor.create<Action>()
|
||||
private fun observeActions() =
|
||||
actions.map { action -> reduce(action, state.value) }
|
||||
.onEach { newState -> _state.value = newState }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
init {
|
||||
addDisposablesToCompositeDisposable()
|
||||
}
|
||||
|
||||
private fun viewStateReducer(): Disposable =
|
||||
actions.map { state.value?.let { value -> reduce(it, value) } }
|
||||
.subscribe(state::postValue, Throwable::printStackTrace)
|
||||
|
||||
protected fun addDisposablesToCompositeDisposable() {
|
||||
compositeDisposable.addAll(
|
||||
viewStateReducer(),
|
||||
pageDao.pages().subscribeOn(Schedulers.io())
|
||||
.subscribe({ actions.offer(UpdatePages(it)) }, Throwable::printStackTrace)
|
||||
)
|
||||
}
|
||||
private fun observePages(dispatcher: CoroutineDispatcher = Dispatchers.IO) =
|
||||
pageDao.pages()
|
||||
.flowOn(dispatcher)
|
||||
.onEach { actions.tryEmit(UpdatePages(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
private fun reduce(action: Action, state: S): S =
|
||||
when (action) {
|
||||
@ -103,7 +110,7 @@ abstract class PageViewModel<T : Page, S : PageState<T>>(
|
||||
): S
|
||||
|
||||
private fun offerShowDeleteDialog(state: S): S {
|
||||
effects.offer(createDeletePageDialogEffect(state, viewModelScope = viewModelScope))
|
||||
effects.tryEmit(createDeletePageDialogEffect(state, viewModelScope = viewModelScope))
|
||||
return state
|
||||
}
|
||||
|
||||
@ -117,9 +124,9 @@ abstract class PageViewModel<T : Page, S : PageState<T>>(
|
||||
return copyWithNewItems(state, state.getItemsAfterToggleSelectionOfItem(action.page))
|
||||
}
|
||||
if (::pageViewModelClickListener.isInitialized) {
|
||||
effects.offer(pageViewModelClickListener.onItemClick(action.page))
|
||||
effects.tryEmit(pageViewModelClickListener.onItemClick(action.page))
|
||||
} else {
|
||||
effects.offer(OpenPage(action.page, zimReaderContainer))
|
||||
effects.tryEmit(OpenPage(action.page, zimReaderContainer))
|
||||
}
|
||||
return state
|
||||
}
|
||||
@ -131,12 +138,15 @@ abstract class PageViewModel<T : Page, S : PageState<T>>(
|
||||
abstract fun deselectAllPages(state: S): S
|
||||
|
||||
private fun exitFragment(state: S): S {
|
||||
effects.offer(PopFragmentBackstack)
|
||||
effects.tryEmit(PopFragmentBackstack)
|
||||
return state
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
compositeDisposable.clear()
|
||||
coroutineJobs.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
coroutineJobs.clear()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
|
@ -21,11 +21,9 @@ package org.kiwix.kiwixmobile.core.page.bookmark.viewmodel
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.reactivex.Flowable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.reactive.asPublisher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
@ -44,6 +42,7 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
|
||||
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
|
||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
|
||||
import org.kiwix.kiwixmobile.core.utils.files.testFlow
|
||||
import org.kiwix.sharedFunctions.InstantExecutorExtension
|
||||
import java.util.UUID
|
||||
|
||||
@ -67,9 +66,7 @@ internal class BookmarkViewModelTest {
|
||||
every { zimReaderContainer.name } returns "zimName"
|
||||
every { sharedPreferenceUtil.showBookmarksAllBooks } returns true
|
||||
every { libkiwixBookMarks.bookmarks() } returns itemsFromDb
|
||||
every { libkiwixBookMarks.pages() } returns Flowable.fromPublisher(
|
||||
libkiwixBookMarks.bookmarks().asPublisher()
|
||||
)
|
||||
every { libkiwixBookMarks.pages() } returns libkiwixBookMarks.bookmarks()
|
||||
viewModel =
|
||||
BookmarkViewModel(libkiwixBookMarks, zimReaderContainer, sharedPreferenceUtil).apply {
|
||||
alertDialogShower = dialogShower
|
||||
@ -106,13 +103,24 @@ internal class BookmarkViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `offerUpdateToShowAllToggle offers UpdateAllBookmarksPreference`() {
|
||||
viewModel.effects.test().also {
|
||||
viewModel.offerUpdateToShowAllToggle(
|
||||
Action.UserClickedShowAllToggle(false),
|
||||
bookmarkState()
|
||||
)
|
||||
}.assertValues(UpdateAllBookmarksPreference(sharedPreferenceUtil, false))
|
||||
fun `offerUpdateToShowAllToggle offers UpdateAllBookmarksPreference`() = runTest {
|
||||
testFlow(
|
||||
flow = viewModel.effects,
|
||||
triggerAction = {
|
||||
viewModel.offerUpdateToShowAllToggle(
|
||||
Action.UserClickedShowAllToggle(false),
|
||||
bookmarkState()
|
||||
)
|
||||
},
|
||||
assert = {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
UpdateAllBookmarksPreference(
|
||||
sharedPreferenceUtil,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -22,9 +22,9 @@ import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
@ -40,7 +40,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedBookmar
|
||||
import java.util.UUID
|
||||
|
||||
internal class ShowDeleteBookmarksDialogTest {
|
||||
val effects = mockk<PublishProcessor<SideEffect<*>>>(relaxed = true)
|
||||
val effects = mockk<MutableSharedFlow<SideEffect<*>>>(relaxed = true)
|
||||
private val newBookmarksDao = mockk<NewBookmarksDao>()
|
||||
val activity = mockk<CoreMainActivity>()
|
||||
private val dialogShower = mockk<DialogShower>(relaxed = true)
|
||||
@ -61,7 +61,7 @@ internal class ShowDeleteBookmarksDialogTest {
|
||||
showDeleteBookmarksDialog.invokeWith(activity)
|
||||
verify { dialogShower.show(any(), capture(lambdaSlot)) }
|
||||
lambdaSlot.captured.invoke()
|
||||
verify { effects.offer(DeletePageItems(bookmarkState(), newBookmarksDao, viewModelScope)) }
|
||||
verify { effects.tryEmit(DeletePageItems(bookmarkState(), newBookmarksDao, viewModelScope)) }
|
||||
}
|
||||
|
||||
private fun mockkActivityInjection(showDeleteBookmarksDialog: ShowDeleteBookmarksDialog) {
|
||||
|
@ -3,12 +3,9 @@ package org.kiwix.kiwixmobile.core.page.history.viewmodel
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.schedulers.TestScheduler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
@ -27,8 +24,8 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
|
||||
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
|
||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
|
||||
import org.kiwix.kiwixmobile.core.utils.files.testFlow
|
||||
import org.kiwix.sharedFunctions.InstantExecutorExtension
|
||||
import org.kiwix.sharedFunctions.setScheduler
|
||||
|
||||
@ExtendWith(InstantExecutorExtension::class)
|
||||
internal class HistoryViewModelTest {
|
||||
@ -39,16 +36,10 @@ internal class HistoryViewModelTest {
|
||||
private val viewModelScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private lateinit var viewModel: HistoryViewModel
|
||||
private val testScheduler = TestScheduler()
|
||||
private val zimReaderSource: ZimReaderSource = mockk()
|
||||
|
||||
init {
|
||||
setScheduler(testScheduler)
|
||||
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
|
||||
}
|
||||
|
||||
private val itemsFromDb: PublishProcessor<List<Page>> =
|
||||
PublishProcessor.create()
|
||||
private val itemsFromDb: MutableSharedFlow<List<Page>> =
|
||||
MutableSharedFlow<List<Page>>(0)
|
||||
|
||||
@BeforeEach
|
||||
fun init() {
|
||||
@ -89,13 +80,21 @@ internal class HistoryViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `offerUpdateToShowAllToggle offers UpdateAllHistoryPreference`() {
|
||||
viewModel.effects.test().also {
|
||||
viewModel.offerUpdateToShowAllToggle(
|
||||
UserClickedShowAllToggle(false),
|
||||
historyState()
|
||||
)
|
||||
}.assertValues(UpdateAllHistoryPreference(sharedPreferenceUtil, false))
|
||||
fun `offerUpdateToShowAllToggle offers UpdateAllHistoryPreference`() = runTest {
|
||||
testFlow(
|
||||
flow = viewModel.effects,
|
||||
triggerAction = {
|
||||
viewModel.offerUpdateToShowAllToggle(
|
||||
UserClickedShowAllToggle(false),
|
||||
historyState()
|
||||
)
|
||||
},
|
||||
assert = {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
UpdateAllHistoryPreference(sharedPreferenceUtil, false)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -4,9 +4,9 @@ import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
@ -20,7 +20,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteAllHistory
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedHistory
|
||||
|
||||
internal class ShowDeleteHistoryDialogTest {
|
||||
val effects = mockk<PublishProcessor<SideEffect<*>>>(relaxed = true)
|
||||
val effects = mockk<MutableSharedFlow<SideEffect<*>>>(relaxed = true)
|
||||
private val historyDao = mockk<HistoryDao>()
|
||||
val activity = mockk<CoreMainActivity>()
|
||||
private val dialogShower = mockk<DialogShower>(relaxed = true)
|
||||
@ -42,7 +42,7 @@ internal class ShowDeleteHistoryDialogTest {
|
||||
showDeleteHistoryDialog.invokeWith(activity)
|
||||
verify { dialogShower.show(any(), capture(lambdaSlot)) }
|
||||
lambdaSlot.captured.invoke()
|
||||
verify { effects.offer(DeletePageItems(historyState(), historyDao, viewModelScope)) }
|
||||
verify { effects.tryEmit(DeletePageItems(historyState(), historyDao, viewModelScope)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -18,18 +18,16 @@
|
||||
|
||||
package org.kiwix.kiwixmobile.core.page.viewmodel
|
||||
|
||||
import com.jraska.livedata.test
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.schedulers.TestScheduler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
@ -40,22 +38,22 @@ import org.kiwix.kiwixmobile.core.dao.PageDao
|
||||
import org.kiwix.kiwixmobile.core.page.PageImpl
|
||||
import org.kiwix.kiwixmobile.core.page.adapter.Page
|
||||
import org.kiwix.kiwixmobile.core.page.pageState
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UpdatePages
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.Filter
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.Exit
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.ExitActionModeMenu
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.Filter
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.OnItemClick
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.OnItemLongClick
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UpdatePages
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UserClickedShowAllToggle
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UserClickedDeleteButton
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UserClickedDeleteSelectedPages
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UserClickedShowAllToggle
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.OnItemClick
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.OnItemLongClick
|
||||
import org.kiwix.kiwixmobile.core.page.viewmodel.effects.OpenPage
|
||||
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
|
||||
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.PopFragmentBackstack
|
||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.core.utils.files.testFlow
|
||||
import org.kiwix.sharedFunctions.InstantExecutorExtension
|
||||
import org.kiwix.sharedFunctions.setScheduler
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@ExtendWith(InstantExecutorExtension::class)
|
||||
@ -65,14 +63,8 @@ internal class PageViewModelTest {
|
||||
private val sharedPreferenceUtil: SharedPreferenceUtil = mockk()
|
||||
|
||||
private lateinit var viewModel: TestablePageViewModel
|
||||
private val testScheduler = TestScheduler()
|
||||
private val itemsFromDb: PublishProcessor<List<Page>> =
|
||||
PublishProcessor.create()
|
||||
|
||||
init {
|
||||
setScheduler(testScheduler)
|
||||
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
|
||||
}
|
||||
private val itemsFromDb: MutableSharedFlow<List<Page>> =
|
||||
MutableSharedFlow<List<Page>>(0)
|
||||
|
||||
@BeforeEach
|
||||
fun init() {
|
||||
@ -91,95 +83,173 @@ internal class PageViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state is Initialising`() {
|
||||
viewModel.state.test().assertValue(pageState())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Exit calls PopFragmentBackstack`() {
|
||||
viewModel.effects.test().also { viewModel.actions.offer(Exit) }
|
||||
.assertValue(PopFragmentBackstack)
|
||||
viewModel.state.test().assertValue(pageState())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ExitActionModeMenu calls deslectAllPages`() {
|
||||
viewModel.actions.offer(ExitActionModeMenu)
|
||||
viewModel.state.test().assertValue(TestablePageState(searchTerm = "deselectAllPagesCalled"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UserClickedShowAllToggle calls offerUpdateToShowAllToggle`() {
|
||||
val action = UserClickedShowAllToggle(true)
|
||||
viewModel.actions.offer(action)
|
||||
viewModel.state.test()
|
||||
.assertValue(TestablePageState(searchTerm = "offerUpdateToShowAllToggleCalled"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UserClickedDeleteButton calls createDeletePageDialogEffect`() {
|
||||
viewModel.actions.offer(UserClickedDeleteButton)
|
||||
assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UserClickedDeleteSelectedPages calls createDeletePageDialogEffect`() {
|
||||
viewModel.actions.offer(UserClickedDeleteSelectedPages)
|
||||
assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun `OnItemClick selects item if one is selected`() {
|
||||
val zimReaderSource: ZimReaderSource = mockk()
|
||||
val page = PageImpl(isSelected = true, zimReaderSource = zimReaderSource)
|
||||
viewModel.state.postValue(TestablePageState(listOf(page)))
|
||||
viewModel.actions.offer(OnItemClick(page))
|
||||
viewModel.state.test()
|
||||
.assertValue(TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource))))
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun `OnItemClick offers OpenPage if none is selected`() {
|
||||
val zimReaderSource: ZimReaderSource = mockk()
|
||||
viewModel.state.postValue(
|
||||
TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))
|
||||
fun `initial state is Initialising`() = runTest {
|
||||
testFlow(
|
||||
flow = viewModel.state,
|
||||
triggerAction = {},
|
||||
assert = { assertThat(awaitItem()).isEqualTo(pageState()) }
|
||||
)
|
||||
viewModel.effects.test()
|
||||
.also { viewModel.actions.offer(OnItemClick(PageImpl(zimReaderSource = zimReaderSource))) }
|
||||
.assertValue(OpenPage(PageImpl(zimReaderSource = zimReaderSource), zimReaderContainer))
|
||||
viewModel.state.test()
|
||||
.assertValue(TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource))))
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun `OnItemLongClick selects item if none is selected`() {
|
||||
fun `Exit calls PopFragmentBackstack`() = runTest {
|
||||
testFlow(
|
||||
flow = viewModel.effects,
|
||||
triggerAction = { viewModel.actions.tryEmit(Exit) },
|
||||
assert = { assertThat(awaitItem()).isEqualTo(PopFragmentBackstack) }
|
||||
)
|
||||
testFlow(
|
||||
flow = viewModel.state,
|
||||
triggerAction = {},
|
||||
assert = { assertThat(awaitItem()).isEqualTo(pageState()) }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ExitActionModeMenu calls deslectAllPages`() = runTest {
|
||||
testFlow(
|
||||
flow = viewModel.state,
|
||||
triggerAction = { viewModel.actions.tryEmit(ExitActionModeMenu) },
|
||||
assert = {
|
||||
assertThat(awaitItem()).isEqualTo(TestablePageState(searchTerm = ""))
|
||||
assertThat(awaitItem())
|
||||
.isEqualTo(TestablePageState(searchTerm = "deselectAllPagesCalled"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UserClickedShowAllToggle calls offerUpdateToShowAllToggle`() = runTest {
|
||||
testFlow(
|
||||
flow = viewModel.state,
|
||||
triggerAction = {
|
||||
viewModel.actions.tryEmit(UserClickedShowAllToggle(true))
|
||||
},
|
||||
assert = {
|
||||
assertThat(awaitItem()).isEqualTo(TestablePageState(searchTerm = ""))
|
||||
assertThat(awaitItem())
|
||||
.isEqualTo(TestablePageState(searchTerm = "offerUpdateToShowAllToggleCalled"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UserClickedDeleteButton calls createDeletePageDialogEffect`() = runTest {
|
||||
viewModel.actions.tryEmit(UserClickedDeleteButton)
|
||||
advanceUntilIdle()
|
||||
assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UserClickedDeleteSelectedPages calls createDeletePageDialogEffect`() = runTest {
|
||||
viewModel.actions.tryEmit(UserClickedDeleteSelectedPages)
|
||||
advanceUntilIdle()
|
||||
assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun `OnItemClick selects item if one is selected`() = runTest {
|
||||
val zimReaderSource: ZimReaderSource = mockk()
|
||||
val page = PageImpl(zimReaderSource = zimReaderSource)
|
||||
viewModel.state.postValue(TestablePageState(listOf(page)))
|
||||
viewModel.actions.offer(OnItemLongClick(page))
|
||||
viewModel.state.test().assertValue(
|
||||
TestablePageState(
|
||||
listOf(
|
||||
PageImpl(
|
||||
isSelected = true,
|
||||
zimReaderSource = zimReaderSource
|
||||
testFlow(
|
||||
viewModel.state,
|
||||
triggerAction = {
|
||||
val page = PageImpl(isSelected = true, zimReaderSource = zimReaderSource)
|
||||
viewModel.getMutableStateForTestCases().value = TestablePageState(listOf(page))
|
||||
viewModel.actions.tryEmit(OnItemClick(page))
|
||||
},
|
||||
assert = {
|
||||
assertThat(awaitItem()).isEqualTo(TestablePageState())
|
||||
assertThat(awaitItem())
|
||||
.isEqualTo(
|
||||
TestablePageState(
|
||||
listOf(PageImpl(zimReaderSource = zimReaderSource))
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun `OnItemClick offers OpenPage if none is selected`() = runTest {
|
||||
val zimReaderSource: ZimReaderSource = mockk()
|
||||
testFlow(
|
||||
viewModel.effects,
|
||||
triggerAction = {
|
||||
viewModel.getMutableStateForTestCases().value =
|
||||
TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))
|
||||
viewModel.actions.tryEmit(OnItemClick(PageImpl(zimReaderSource = zimReaderSource)))
|
||||
},
|
||||
assert = {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
OpenPage(
|
||||
PageImpl(zimReaderSource = zimReaderSource),
|
||||
zimReaderContainer
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
testFlow(
|
||||
viewModel.state,
|
||||
triggerAction = {
|
||||
viewModel.getMutableStateForTestCases().value =
|
||||
TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))
|
||||
viewModel.actions.tryEmit(OnItemClick(PageImpl(zimReaderSource = zimReaderSource)))
|
||||
},
|
||||
assert = {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Filter calls updatePagesBasedOnFilter`() {
|
||||
viewModel.actions.offer(Filter("Called"))
|
||||
viewModel.state.test()
|
||||
.assertValue(TestablePageState(searchTerm = "updatePagesBasedOnFilterCalled"))
|
||||
internal fun `OnItemLongClick selects item if none is selected`() = runTest {
|
||||
val zimReaderSource: ZimReaderSource = mockk()
|
||||
val page = PageImpl(zimReaderSource = zimReaderSource)
|
||||
testFlow(
|
||||
viewModel.state,
|
||||
triggerAction = {
|
||||
viewModel.getMutableStateForTestCases().value = TestablePageState(listOf(page))
|
||||
viewModel.actions.tryEmit(OnItemLongClick(page))
|
||||
},
|
||||
assert = {
|
||||
assertThat(awaitItem()).isEqualTo(TestablePageState())
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
TestablePageState(
|
||||
listOf(
|
||||
PageImpl(
|
||||
isSelected = true,
|
||||
zimReaderSource = zimReaderSource
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UpdatePages calls updatePages`() {
|
||||
viewModel.actions.offer(UpdatePages(emptyList()))
|
||||
viewModel.state.test()
|
||||
.assertValue(TestablePageState(searchTerm = "updatePagesCalled"))
|
||||
fun `Filter calls updatePagesBasedOnFilter`() = runTest {
|
||||
testFlow(
|
||||
viewModel.state,
|
||||
triggerAction = { viewModel.actions.tryEmit(Filter("Called")) },
|
||||
assert = {
|
||||
assertThat(awaitItem()).isEqualTo(TestablePageState())
|
||||
assertThat(awaitItem()).isEqualTo(TestablePageState(searchTerm = "updatePagesBasedOnFilterCalled"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UpdatePages calls updatePages`() = runTest {
|
||||
testFlow(
|
||||
viewModel.state,
|
||||
triggerAction = { viewModel.actions.tryEmit(UpdatePages(emptyList())) },
|
||||
assert = {
|
||||
assertThat(awaitItem()).isEqualTo(TestablePageState())
|
||||
assertThat(awaitItem()).isEqualTo(TestablePageState(searchTerm = "updatePagesCalled"))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
@ -41,8 +40,6 @@ import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.kiwix.sharedFunctions.resetSchedulers
|
||||
import org.kiwix.sharedFunctions.setScheduler
|
||||
import java.io.File
|
||||
|
||||
class FileSearchTest {
|
||||
@ -54,10 +51,6 @@ class FileSearchTest {
|
||||
private val storageDevice: StorageDevice = mockk()
|
||||
private val scanningProgressListener: ScanningProgressListener = mockk()
|
||||
|
||||
init {
|
||||
setScheduler(Schedulers.trampoline())
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun init() {
|
||||
clearMocks(context, externalStorageDirectory, contentResolver, storageDevice)
|
||||
@ -78,7 +71,6 @@ class FileSearchTest {
|
||||
@AfterAll
|
||||
fun teardown() {
|
||||
deleteTempDirectory()
|
||||
resetSchedulers()
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
Loading…
x
Reference in New Issue
Block a user