Merge pull request #4322 from kiwix/Fixes#4308

Refactored the `RxJava` to coroutines in `PageViewModel`.
This commit is contained in:
Kelson 2025-05-16 16:30:38 +02:00 committed by GitHub
commit 307e7249de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 428 additions and 339 deletions

View File

@ -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)
}

View File

@ -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(

View File

@ -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))
}
}

View File

@ -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>)
}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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()
}
}

View File

@ -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>)

View File

@ -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 ->

View File

@ -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>)

View File

@ -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))

View File

@ -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)

View File

@ -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>)

View File

@ -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>)

View File

@ -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>)
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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)) }
)
}
}

View File

@ -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)
}

View File

@ -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))
})
}
}

View File

@ -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)
}

View File

@ -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))
}
)
}

View File

@ -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))
}
)
}

View File

@ -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

View File

@ -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()
}

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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"))
}
)
}
}

View File

@ -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