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 // test inserting into history database
historyRoomDao.saveHistory(historyItem) historyRoomDao.saveHistory(historyItem)
var historyList = historyRoomDao.historyRoomEntity().blockingFirst() var historyList = historyRoomDao.historyRoomEntity().first()
with(historyList.first()) { with(historyList.first()) {
assertThat(historyTitle, equalTo(historyItem.title)) assertThat(historyTitle, equalTo(historyItem.title))
assertThat(zimId, equalTo(historyItem.zimId)) assertThat(zimId, equalTo(historyItem.zimId))
@ -126,7 +126,7 @@ class KiwixRoomDatabaseTest {
// test deleting the history // test deleting the history
historyRoomDao.deleteHistory(listOf(historyItem)) historyRoomDao.deleteHistory(listOf(historyItem))
historyList = historyRoomDao.historyRoomEntity().blockingFirst() historyList = historyRoomDao.historyRoomEntity().first()
assertEquals(historyList.size, 0) assertEquals(historyList.size, 0)
// test deleting all history // test deleting all history
@ -134,10 +134,10 @@ class KiwixRoomDatabaseTest {
historyRoomDao.saveHistory( historyRoomDao.saveHistory(
getHistoryItem(databaseId = 2) getHistoryItem(databaseId = 2)
) )
historyList = historyRoomDao.historyRoomEntity().blockingFirst() historyList = historyRoomDao.historyRoomEntity().first()
assertEquals(historyList.size, 2) assertEquals(historyList.size, 2)
historyRoomDao.deleteAllHistory() historyRoomDao.deleteAllHistory()
historyList = historyRoomDao.historyRoomEntity().blockingFirst() historyList = historyRoomDao.historyRoomEntity().first()
assertEquals(historyList.size, 0) assertEquals(historyList.size, 0)
} }
@ -146,7 +146,7 @@ class KiwixRoomDatabaseTest {
runBlocking { runBlocking {
notesRoomDao = db.notesRoomDao() notesRoomDao = db.notesRoomDao()
// delete all the notes from database to properly run the test cases. // 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 = val noteItem =
getNoteListItem( getNoteListItem(
zimUrl = "http://kiwix.app/MainPage", zimUrl = "http://kiwix.app/MainPage",
@ -155,7 +155,7 @@ class KiwixRoomDatabaseTest {
// Save and retrieve a notes item // Save and retrieve a notes item
notesRoomDao.saveNote(noteItem) notesRoomDao.saveNote(noteItem)
var notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem> var notesList = notesRoomDao.notes().first() as List<NoteListItem>
with(notesList.first()) { with(notesList.first()) {
assertThat(zimId, equalTo(noteItem.zimId)) assertThat(zimId, equalTo(noteItem.zimId))
assertThat(zimUrl, equalTo(noteItem.zimUrl)) assertThat(zimUrl, equalTo(noteItem.zimUrl))
@ -168,7 +168,7 @@ class KiwixRoomDatabaseTest {
// test deleting the history // test deleting the history
notesRoomDao.deleteNotes(listOf(noteItem)) notesRoomDao.deleteNotes(listOf(noteItem))
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem> notesList = notesRoomDao.notes().first() as List<NoteListItem>
assertEquals(notesList.size, 0) assertEquals(notesList.size, 0)
// test deleting all notes // test deleting all notes
@ -179,10 +179,10 @@ class KiwixRoomDatabaseTest {
zimUrl = "http://kiwix.app/Installing" zimUrl = "http://kiwix.app/Installing"
) )
) )
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem> notesList = notesRoomDao.notes().first() as List<NoteListItem>
assertEquals(notesList.size, 2) assertEquals(notesList.size, 2)
notesRoomDao.deletePages(notesRoomDao.notes().blockingFirst()) notesRoomDao.deletePages(notesRoomDao.notes().first())
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem> notesList = notesRoomDao.notes().first() as List<NoteListItem>
assertEquals(notesList.size, 0) assertEquals(notesList.size, 0)
} }

View File

@ -215,7 +215,7 @@ class ObjectBoxToRoomMigratorTest {
kiwixRoomDatabase.recentSearchRoomDao().deleteSearchHistory() kiwixRoomDatabase.recentSearchRoomDao().deleteSearchHistory()
kiwixRoomDatabase.historyRoomDao().deleteAllHistory() kiwixRoomDatabase.historyRoomDao().deleteAllHistory()
kiwixRoomDatabase.notesRoomDao() kiwixRoomDatabase.notesRoomDao()
.deletePages(kiwixRoomDatabase.notesRoomDao().notes().blockingFirst()) .deletePages(kiwixRoomDatabase.notesRoomDao().notes().first())
box.removeAll() box.removeAll()
} }
@ -238,7 +238,7 @@ class ObjectBoxToRoomMigratorTest {
// migrate data into room database // migrate data into room database
objectBoxToRoomMigrator.migrateHistory(box) objectBoxToRoomMigrator.migrateHistory(box)
// check if data successfully migrated to room // check if data successfully migrated to room
val actual = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() val actual = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
with(actual.first()) { with(actual.first()) {
assertThat(historyTitle, equalTo(historyItem.title)) assertThat(historyTitle, equalTo(historyItem.title))
assertThat(zimId, equalTo(historyItem.zimId)) assertThat(zimId, equalTo(historyItem.zimId))
@ -254,7 +254,7 @@ class ObjectBoxToRoomMigratorTest {
// Migrate data from empty ObjectBox database // Migrate data from empty ObjectBox database
objectBoxToRoomMigrator.migrateHistory(box) objectBoxToRoomMigrator.migrateHistory(box)
var actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() var actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
assertTrue(actualData.isEmpty()) assertTrue(actualData.isEmpty())
// Test if data successfully migrated to Room and existing data is preserved // Test if data successfully migrated to Room and existing data is preserved
@ -262,7 +262,7 @@ class ObjectBoxToRoomMigratorTest {
box.put(HistoryEntity(historyItem2)) box.put(HistoryEntity(historyItem2))
// Migrate data into Room database // Migrate data into Room database
objectBoxToRoomMigrator.migrateHistory(box) objectBoxToRoomMigrator.migrateHistory(box)
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
assertEquals(2, actualData.size) assertEquals(2, actualData.size)
val existingItem = val existingItem =
actualData.find { actualData.find {
@ -281,7 +281,7 @@ class ObjectBoxToRoomMigratorTest {
kiwixRoomDatabase.historyRoomDao().saveHistory(historyItem) kiwixRoomDatabase.historyRoomDao().saveHistory(historyItem)
box.put(HistoryEntity(historyItem)) box.put(HistoryEntity(historyItem))
objectBoxToRoomMigrator.migrateHistory(box) objectBoxToRoomMigrator.migrateHistory(box)
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
assertEquals(1, actualData.size) assertEquals(1, actualData.size)
clearRoomAndBoxStoreDatabases(box) clearRoomAndBoxStoreDatabases(box)
@ -296,7 +296,7 @@ class ObjectBoxToRoomMigratorTest {
kiwixRoomDatabase.historyRoomDao().saveHistory(historyItem4) kiwixRoomDatabase.historyRoomDao().saveHistory(historyItem4)
box.put(HistoryEntity(historyItem)) box.put(HistoryEntity(historyItem))
objectBoxToRoomMigrator.migrateHistory(box) objectBoxToRoomMigrator.migrateHistory(box)
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
assertEquals(2, actualData.size) assertEquals(2, actualData.size)
clearRoomAndBoxStoreDatabases(box) clearRoomAndBoxStoreDatabases(box)
@ -310,7 +310,7 @@ class ObjectBoxToRoomMigratorTest {
} catch (_: Exception) { } catch (_: Exception) {
} }
// Ensure Room database remains empty or unaffected by the invalid data // Ensure Room database remains empty or unaffected by the invalid data
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
assertTrue(actualData.isEmpty()) assertTrue(actualData.isEmpty())
// Test large data migration for recent searches // Test large data migration for recent searches
@ -332,7 +332,7 @@ class ObjectBoxToRoomMigratorTest {
val endTime = System.currentTimeMillis() val endTime = System.currentTimeMillis()
val migrationTime = endTime - startTime val migrationTime = endTime - startTime
// Check if data successfully migrated to Room // Check if data successfully migrated to Room
actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first()
assertEquals(numEntities, actualData.size) assertEquals(numEntities, actualData.size)
// Assert that the migration completes within a reasonable time frame // Assert that the migration completes within a reasonable time frame
assertTrue( assertTrue(
@ -367,7 +367,7 @@ class ObjectBoxToRoomMigratorTest {
// migrate data into room database // migrate data into room database
objectBoxToRoomMigrator.migrateNotes(box) objectBoxToRoomMigrator.migrateNotes(box)
// check if data successfully migrated to room // 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()) { with(notesList.first()) {
assertThat(zimId, equalTo(noteItem.zimId)) assertThat(zimId, equalTo(noteItem.zimId))
assertThat(zimUrl, equalTo(noteItem.zimUrl)) assertThat(zimUrl, equalTo(noteItem.zimUrl))
@ -382,7 +382,7 @@ class ObjectBoxToRoomMigratorTest {
// Migrate data from empty ObjectBox database // Migrate data from empty ObjectBox database
objectBoxToRoomMigrator.migrateNotes(box) objectBoxToRoomMigrator.migrateNotes(box)
notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List<NoteListItem> notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List<NoteListItem>
assertTrue(notesList.isEmpty()) assertTrue(notesList.isEmpty())
// Test if data successfully migrated to Room and existing data is preserved // Test if data successfully migrated to Room and existing data is preserved
@ -390,7 +390,7 @@ class ObjectBoxToRoomMigratorTest {
box.put(NotesEntity(noteItem)) box.put(NotesEntity(noteItem))
// Migrate data into Room database // Migrate data into Room database
objectBoxToRoomMigrator.migrateNotes(box) 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(noteItem.title, notesList.first().title)
assertEquals(2, notesList.size) assertEquals(2, notesList.size)
val existingItem = val existingItem =
@ -411,7 +411,7 @@ class ObjectBoxToRoomMigratorTest {
box.put(NotesEntity(noteItem1)) box.put(NotesEntity(noteItem1))
// Migrate data into Room database // Migrate data into Room database
objectBoxToRoomMigrator.migrateNotes(box) objectBoxToRoomMigrator.migrateNotes(box)
notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List<NoteListItem> notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List<NoteListItem>
assertEquals(1, notesList.size) assertEquals(1, notesList.size)
clearRoomAndBoxStoreDatabases(box) clearRoomAndBoxStoreDatabases(box)
@ -426,7 +426,7 @@ class ObjectBoxToRoomMigratorTest {
kiwixRoomDatabase.notesRoomDao().saveNote(noteItem1) kiwixRoomDatabase.notesRoomDao().saveNote(noteItem1)
box.put(NotesEntity(noteItem2)) box.put(NotesEntity(noteItem2))
objectBoxToRoomMigrator.migrateNotes(box) objectBoxToRoomMigrator.migrateNotes(box)
notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List<NoteListItem> notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List<NoteListItem>
assertEquals(2, notesList.size) assertEquals(2, notesList.size)
clearRoomAndBoxStoreDatabases(box) clearRoomAndBoxStoreDatabases(box)
@ -440,7 +440,7 @@ class ObjectBoxToRoomMigratorTest {
} catch (_: Exception) { } catch (_: Exception) {
} }
// Ensure Room database remains empty or unaffected by the invalid data // 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()) assertTrue(notesList.isEmpty())
// Test large data migration for recent searches // Test large data migration for recent searches
@ -462,7 +462,7 @@ class ObjectBoxToRoomMigratorTest {
val endTime = System.currentTimeMillis() val endTime = System.currentTimeMillis()
val migrationTime = endTime - startTime val migrationTime = endTime - startTime
// Check if data successfully migrated to Room // 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) assertEquals(numEntities, notesList.size)
// Assert that the migration completes within a reasonable time frame // Assert that the migration completes within a reasonable time frame
assertTrue( assertTrue(

View File

@ -22,6 +22,7 @@ import android.content.Context
import androidx.room.Room import androidx.room.Room
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.core.IsEqual.equalTo import org.hamcrest.core.IsEqual.equalTo
@ -66,7 +67,7 @@ class HistoryRoomDaoTest {
// Save and retrieve a history item // Save and retrieve a history item
historyRoomDao.saveHistory(historyItem) historyRoomDao.saveHistory(historyItem)
var historyList = historyRoomDao.historyRoomEntity().blockingFirst() var historyList = historyRoomDao.historyRoomEntity().first()
with(historyList.first()) { with(historyList.first()) {
assertThat(historyTitle, equalTo(historyItem.title)) assertThat(historyTitle, equalTo(historyItem.title))
assertThat(zimId, equalTo(historyItem.zimId)) assertThat(zimId, equalTo(historyItem.zimId))
@ -80,26 +81,26 @@ class HistoryRoomDaoTest {
// Test to update the same day history for url // Test to update the same day history for url
historyRoomDao.saveHistory(historyItem) historyRoomDao.saveHistory(historyItem)
historyList = historyRoomDao.historyRoomEntity().blockingFirst() historyList = historyRoomDao.historyRoomEntity().first()
assertEquals(historyList.size, 1) assertEquals(historyList.size, 1)
// Delete the saved history item // Delete the saved history item
historyRoomDao.deleteHistory(listOf(historyItem)) historyRoomDao.deleteHistory(listOf(historyItem))
historyList = historyRoomDao.historyRoomEntity().blockingFirst() historyList = historyRoomDao.historyRoomEntity().first()
assertEquals(historyList.size, 0) assertEquals(historyList.size, 0)
// Save and delete all history items // Save and delete all history items
historyRoomDao.saveHistory(historyItem) historyRoomDao.saveHistory(historyItem)
historyRoomDao.saveHistory(getHistoryItem(databaseId = 2, dateString = "31 May 2024")) historyRoomDao.saveHistory(getHistoryItem(databaseId = 2, dateString = "31 May 2024"))
historyRoomDao.deleteAllHistory() historyRoomDao.deleteAllHistory()
historyList = historyRoomDao.historyRoomEntity().blockingFirst() historyList = historyRoomDao.historyRoomEntity().first()
assertThat(historyList.size, equalTo(0)) assertThat(historyList.size, equalTo(0))
// Save history item with empty fields // Save history item with empty fields
val emptyHistoryUrl = "" val emptyHistoryUrl = ""
val emptyTitle = "" val emptyTitle = ""
historyRoomDao.saveHistory(getHistoryItem(emptyTitle, emptyHistoryUrl, databaseId = 1)) historyRoomDao.saveHistory(getHistoryItem(emptyTitle, emptyHistoryUrl, databaseId = 1))
historyList = historyRoomDao.historyRoomEntity().blockingFirst() historyList = historyRoomDao.historyRoomEntity().first()
assertThat(historyList.size, equalTo(1)) assertThat(historyList.size, equalTo(1))
historyRoomDao.deleteAllHistory() historyRoomDao.deleteAllHistory()
@ -113,14 +114,14 @@ class HistoryRoomDaoTest {
dateString = "31 May 2024" dateString = "31 May 2024"
) )
historyRoomDao.saveHistory(historyItem1) historyRoomDao.saveHistory(historyItem1)
historyList = historyRoomDao.historyRoomEntity().blockingFirst() historyList = historyRoomDao.historyRoomEntity().first()
assertThat(historyList.size, equalTo(2)) assertThat(historyList.size, equalTo(2))
historyRoomDao.deleteAllHistory() historyRoomDao.deleteAllHistory()
// Save two entity with same and database id with same date to see if it's updated or not. // 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)
historyRoomDao.saveHistory(historyItem) historyRoomDao.saveHistory(historyItem)
historyList = historyRoomDao.historyRoomEntity().blockingFirst() historyList = historyRoomDao.historyRoomEntity().first()
assertThat(historyList.size, equalTo(1)) assertThat(historyList.size, equalTo(1))
historyRoomDao.deleteAllHistory() historyRoomDao.deleteAllHistory()
@ -132,7 +133,7 @@ class HistoryRoomDaoTest {
"Undefined value was saved into database", "Undefined value was saved into database",
false false
) )
} catch (e: Exception) { } catch (_: Exception) {
assertThat("Undefined value was not saved, as expected.", true) 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 unicodeTitle = "title \u03A3" // Unicode character for Greek capital letter Sigma
val historyItem2 = getHistoryItem(title = unicodeTitle, databaseId = 2) val historyItem2 = getHistoryItem(title = unicodeTitle, databaseId = 2)
historyRoomDao.saveHistory(historyItem2) historyRoomDao.saveHistory(historyItem2)
historyList = historyRoomDao.historyRoomEntity().blockingFirst() historyList = historyRoomDao.historyRoomEntity().first()
assertThat(historyList.first().historyTitle, equalTo("title Σ")) assertThat(historyList.first().historyTitle, equalTo("title Σ"))
// Test deletePages function // Test deletePages function
historyRoomDao.saveHistory(historyItem) historyRoomDao.saveHistory(historyItem)
historyRoomDao.deletePages(listOf(historyItem, historyItem2)) historyRoomDao.deletePages(listOf(historyItem, historyItem2))
historyList = historyRoomDao.historyRoomEntity().blockingFirst() historyList = historyRoomDao.historyRoomEntity().first()
assertThat(historyList.size, equalTo(0)) assertThat(historyList.size, equalTo(0))
} }
} }

View File

@ -22,6 +22,7 @@ import android.content.Context
import androidx.room.Room import androidx.room.Room
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.core.IsEqual.equalTo import org.hamcrest.core.IsEqual.equalTo
@ -65,7 +66,7 @@ class NoteRoomDaoTest {
// Save and retrieve a notes item // Save and retrieve a notes item
notesRoomDao.saveNote(noteItem) notesRoomDao.saveNote(noteItem)
var notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem> var notesList = notesRoomDao.notes().first() as List<NoteListItem>
with(notesList.first()) { with(notesList.first()) {
assertThat(zimId, equalTo(noteItem.zimId)) assertThat(zimId, equalTo(noteItem.zimId))
assertThat(zimUrl, equalTo(noteItem.zimUrl)) assertThat(zimUrl, equalTo(noteItem.zimUrl))
@ -78,25 +79,25 @@ class NoteRoomDaoTest {
// Test update the existing note // Test update the existing note
notesRoomDao.saveNote(noteItem) notesRoomDao.saveNote(noteItem)
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem> notesList = notesRoomDao.notes().first() as List<NoteListItem>
assertEquals(notesList.size, 1) assertEquals(notesList.size, 1)
// Delete the saved note item with all delete methods available in NoteRoomDao. // Delete the saved note item with all delete methods available in NoteRoomDao.
// delete via noteTitle // delete via noteTitle
notesRoomDao.deleteNote(noteItem.title) notesRoomDao.deleteNote(noteItem.title)
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem> notesList = notesRoomDao.notes().first() as List<NoteListItem>
assertEquals(notesList.size, 0) assertEquals(notesList.size, 0)
// delete with deletePages method // delete with deletePages method
notesRoomDao.saveNote(noteItem) notesRoomDao.saveNote(noteItem)
notesRoomDao.deletePages(listOf(noteItem)) notesRoomDao.deletePages(listOf(noteItem))
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem> notesList = notesRoomDao.notes().first() as List<NoteListItem>
assertEquals(notesList.size, 0) assertEquals(notesList.size, 0)
// delete with list of NoteListItem // delete with list of NoteListItem
notesRoomDao.saveNote(noteItem) notesRoomDao.saveNote(noteItem)
notesRoomDao.deleteNotes(listOf(noteItem)) notesRoomDao.deleteNotes(listOf(noteItem))
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem> notesList = notesRoomDao.notes().first() as List<NoteListItem>
assertEquals(notesList.size, 0) assertEquals(notesList.size, 0)
// Save note with empty title // Save note with empty title
@ -107,7 +108,7 @@ class NoteRoomDaoTest {
noteFilePath = "/storage/emulated/0/Download/Notes/Alpine linux/Installing.txt" 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) assertEquals(notesList.size, 1)
clearNotes() clearNotes()
@ -127,7 +128,7 @@ class NoteRoomDaoTest {
) )
kiwixRoomDatabase.notesRoomDao().saveNote(noteItem2) kiwixRoomDatabase.notesRoomDao().saveNote(noteItem2)
kiwixRoomDatabase.notesRoomDao().saveNote(noteItem3) kiwixRoomDatabase.notesRoomDao().saveNote(noteItem3)
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem> notesList = notesRoomDao.notes().first() as List<NoteListItem>
assertEquals(2, notesList.size) assertEquals(2, notesList.size)
clearNotes() clearNotes()
@ -139,7 +140,7 @@ class NoteRoomDaoTest {
"Undefined value was saved into database", "Undefined value was saved into database",
false false
) )
} catch (e: Exception) { } catch (_: Exception) {
assertThat("Undefined value was not saved, as expected.", true) assertThat("Undefined value was not saved, as expected.", true)
} }
@ -148,11 +149,11 @@ class NoteRoomDaoTest {
val noteListItem2 = val noteListItem2 =
getNoteListItem(title = unicodeTitle, zimUrl = "http://kiwix.app/Installing") getNoteListItem(title = unicodeTitle, zimUrl = "http://kiwix.app/Installing")
notesRoomDao.saveNote(noteListItem2) notesRoomDao.saveNote(noteListItem2)
notesList = notesRoomDao.notes().blockingFirst() as List<NoteListItem> notesList = notesRoomDao.notes().first() as List<NoteListItem>
assertThat(notesList.first().title, equalTo("title Σ")) assertThat(notesList.first().title, equalTo("title Σ"))
} }
private suspend fun clearNotes() { 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.runtime.setValue
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.reactivex.disposables.CompositeDisposable
import org.kiwix.kiwixmobile.cachedComponent import org.kiwix.kiwixmobile.cachedComponent
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseActivity
@ -55,7 +54,6 @@ class LanguageFragment : BaseFragment() {
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
private var composeView: ComposeView? = null private var composeView: ComposeView? = null
private val compositeDisposable = CompositeDisposable()
override fun inject(baseActivity: BaseActivity) { override fun inject(baseActivity: BaseActivity) {
baseActivity.cachedComponent.inject(this) baseActivity.cachedComponent.inject(this)
@ -153,7 +151,6 @@ class LanguageFragment : BaseFragment() {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
compositeDisposable.clear()
composeView?.disposeComposition() composeView?.disposeComposition()
composeView = null composeView = null
} }

View File

@ -25,8 +25,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map 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.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
@ -55,21 +56,17 @@ class LanguageViewModel @Inject constructor(
} }
private fun observeActions() = private fun observeActions() =
viewModelScope.launch {
actions actions
.map { action -> reduce(action, state.value) } .map { action -> reduce(action, state.value) }
.distinctUntilChanged() .distinctUntilChanged()
.collect { newState -> state.value = newState } .onEach { newState -> state.value = newState }
} .launchIn(viewModelScope)
private fun observeLanguages() = private fun observeLanguages() =
viewModelScope.launch {
languageDao.languages() languageDao.languages()
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.collect { languages -> .onEach { languages -> actions.tryEmit(UpdateLanguages(languages)) }
actions.tryEmit(UpdateLanguages(languages)) .launchIn(viewModelScope)
}
}
override fun onCleared() { override fun onCleared() {
coroutineJobs.forEach { coroutineJobs.forEach {

View File

@ -17,7 +17,6 @@
*/ */
package org.kiwix.kiwixmobile.core.base 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.Presenter
import org.kiwix.kiwixmobile.core.base.BaseContract.View import org.kiwix.kiwixmobile.core.base.BaseContract.View
@ -26,9 +25,6 @@ import org.kiwix.kiwixmobile.core.base.BaseContract.View
*/ */
@Suppress("UnnecessaryAbstractClass") @Suppress("UnnecessaryAbstractClass")
abstract class BasePresenter<T : View<*>?> : Presenter<T> { abstract class BasePresenter<T : View<*>?> : Presenter<T> {
@JvmField
val compositeDisposable = CompositeDisposable()
@JvmField @JvmField
var view: T? = null var view: T? = null
@ -38,6 +34,5 @@ abstract class BasePresenter<T : View<*>?> : Presenter<T> {
override fun detachView() { override fun detachView() {
view = null view = null
compositeDisposable.clear()
} }
} }

View File

@ -20,7 +20,8 @@ package org.kiwix.kiwixmobile.core.dao
import io.objectbox.Box import io.objectbox.Box
import io.objectbox.kotlin.query import io.objectbox.kotlin.query
import io.objectbox.query.QueryBuilder 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.dao.entities.HistoryEntity_ import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity_
import org.kiwix.kiwixmobile.core.page.adapter.Page 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 import javax.inject.Inject
class HistoryDao @Inject constructor(val box: Box<HistoryEntity>) : PageDao { class HistoryDao @Inject constructor(val box: Box<HistoryEntity>) : PageDao {
fun history(): Flowable<List<Page>> = fun history(): Flow<List<Page>> =
box.asFlowable( box.asFlow(
box.query { box.query {
orderDesc(HistoryEntity_.timeStamp) 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>) = override fun deletePages(pagesToDelete: List<Page>) =
deleteHistory(pagesToDelete as List<HistoryItem>) deleteHistory(pagesToDelete as List<HistoryItem>)

View File

@ -24,7 +24,8 @@ import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.TypeConverter import androidx.room.TypeConverter
import androidx.room.Update 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.dao.entities.HistoryRoomEntity
import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem
@ -33,9 +34,9 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
@Dao @Dao
abstract class HistoryRoomDao : PageDao { abstract class HistoryRoomDao : PageDao {
@Query("SELECT * FROM HistoryRoomEntity ORDER BY HistoryRoomEntity.timeStamp DESC") @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 { historyRoomEntity().map {
it.map { historyEntity -> it.map { historyEntity ->
historyEntity.zimFilePath?.let { filePath -> historyEntity.zimFilePath?.let { filePath ->

View File

@ -21,7 +21,6 @@ package org.kiwix.kiwixmobile.core.dao
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.util.Base64 import android.util.Base64
import io.reactivex.Flowable
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -30,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asPublisher
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.CoreApp
@ -121,10 +119,7 @@ class LibkiwixBookmarks @Inject constructor(
bookmarkListFlow bookmarkListFlow
.map { it } .map { it }
// Currently kept in RxJava Flowable because `PageViewModel` still expects RxJava streams. override fun pages(): Flow<List<Page>> = bookmarks()
// 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 deletePages(pagesToDelete: List<Page>) = override fun deletePages(pagesToDelete: List<Page>) =
deleteBookmarks(pagesToDelete as List<LibkiwixBookmarkItem>) deleteBookmarks(pagesToDelete as List<LibkiwixBookmarkItem>)

View File

@ -20,8 +20,11 @@ package org.kiwix.kiwixmobile.core.dao
import io.objectbox.Box import io.objectbox.Box
import io.objectbox.kotlin.query import io.objectbox.kotlin.query
import io.objectbox.query.QueryBuilder import io.objectbox.query.QueryBuilder
import io.reactivex.Flowable import kotlinx.coroutines.CoroutineDispatcher
import io.reactivex.schedulers.Schedulers 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.dao.entities.BookmarkEntity_ import org.kiwix.kiwixmobile.core.dao.entities.BookmarkEntity_
import org.kiwix.kiwixmobile.core.page.adapter.Page 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 import javax.inject.Inject
class NewBookmarksDao @Inject constructor(val box: Box<BookmarkEntity>) : PageDao { class NewBookmarksDao @Inject constructor(val box: Box<BookmarkEntity>) : PageDao {
fun bookmarks(): Flowable<List<Page>> = fun bookmarks(): Flow<List<Page>> =
box.asFlowable( box.asFlow(
box.query { box.query {
order(BookmarkEntity_.bookmarkTitle) 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>) = override fun deletePages(pagesToDelete: List<Page>) =
deleteBookmarks(pagesToDelete as List<BookmarkItem>) deleteBookmarks(pagesToDelete as List<BookmarkItem>)
@ -71,8 +74,11 @@ class NewBookmarksDao @Inject constructor(val box: Box<BookmarkEntity>) : PageDa
.toList() .toList()
.distinct() .distinct()
fun bookmarkUrlsForCurrentBook(zimFileReader: ZimFileReader?): Flowable<List<String>> = fun bookmarkUrlsForCurrentBook(
box.asFlowable( zimFileReader: ZimFileReader?,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): Flow<List<String>> =
box.asFlow(
box.query { box.query {
equal( equal(
BookmarkEntity_.zimId, BookmarkEntity_.zimId,
@ -88,7 +94,7 @@ class NewBookmarksDao @Inject constructor(val box: Box<BookmarkEntity>) : PageDa
order(BookmarkEntity_.bookmarkTitle) order(BookmarkEntity_.bookmarkTitle)
} }
).map { it.map(BookmarkEntity::bookmarkUrl) } ).map { it.map(BookmarkEntity::bookmarkUrl) }
.subscribeOn(Schedulers.io()) .flowOn(dispatcher)
fun saveBookmark(bookmarkItem: BookmarkItem) { fun saveBookmark(bookmarkItem: BookmarkItem) {
box.put(BookmarkEntity(bookmarkItem)) box.put(BookmarkEntity(bookmarkItem))

View File

@ -21,9 +21,6 @@ import io.objectbox.Box
import io.objectbox.kotlin.flow import io.objectbox.kotlin.flow
import io.objectbox.kotlin.query import io.objectbox.kotlin.query
import io.objectbox.query.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.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -53,9 +50,3 @@ fun <T> Box<T>.asFlow(query: Query<T> = query {}): Flow<List<T>> {
.map { it.toList() } .map { it.toList() }
.distinctUntilChanged() .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.Box
import io.objectbox.kotlin.query import io.objectbox.kotlin.query
import io.objectbox.query.QueryBuilder 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.dao.entities.NotesEntity_ import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity_
import org.kiwix.kiwixmobile.core.page.adapter.Page 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 import javax.inject.Inject
class NewNoteDao @Inject constructor(val box: Box<NotesEntity>) : PageDao { class NewNoteDao @Inject constructor(val box: Box<NotesEntity>) : PageDao {
fun notes(): Flowable<List<Page>> = fun notes(): Flow<List<Page>> =
box.asFlowable( box.asFlow(
box.query { box.query {
order(NotesEntity_.noteTitle) 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>) = override fun deletePages(pagesToDelete: List<Page>) =
deleteNotes(pagesToDelete as List<NoteListItem>) deleteNotes(pagesToDelete as List<NoteListItem>)

View File

@ -22,10 +22,11 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import io.reactivex.Flowable
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.dao.entities.NotesRoomEntity import org.kiwix.kiwixmobile.core.dao.entities.NotesRoomEntity
import org.kiwix.kiwixmobile.core.extensions.deleteFile import org.kiwix.kiwixmobile.core.extensions.deleteFile
@ -38,9 +39,9 @@ import java.io.File
@Dao @Dao
abstract class NotesRoomDao : PageDao { abstract class NotesRoomDao : PageDao {
@Query("SELECT * FROM NotesRoomEntity ORDER BY NotesRoomEntity.noteTitle") @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 { notesAsEntity().map {
it.map { notesEntity -> it.map { notesEntity ->
notesEntity.zimFilePath?.let { filePath -> 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>) = override fun deletePages(pagesToDelete: List<Page>) =
deleteNotes(pagesToDelete as List<NoteListItem>) deleteNotes(pagesToDelete as List<NoteListItem>)

View File

@ -18,10 +18,10 @@
package org.kiwix.kiwixmobile.core.dao package org.kiwix.kiwixmobile.core.dao
import io.reactivex.Flowable import kotlinx.coroutines.flow.Flow
import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.adapter.Page
interface PageDao { interface PageDao {
fun pages(): Flowable<List<Page>> fun pages(): Flow<List<Page>>
fun deletePages(pagesToDelete: 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.mutableStateOf
import androidx.compose.runtime.referentialEqualityPolicy import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider 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.R
import org.kiwix.kiwixmobile.core.base.BaseFragment import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO 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.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.page.adapter.OnItemClickListener import org.kiwix.kiwixmobile.core.page.adapter.OnItemClickListener
import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.adapter.Page
@ -66,7 +69,7 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
@Inject lateinit var alertDialogShower: AlertDialogShower @Inject lateinit var alertDialogShower: AlertDialogShower
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
val compositeDisposable = CompositeDisposable() private val coroutineJobs = mutableListOf<Job>()
abstract val screenTitle: Int abstract val screenTitle: Int
abstract val noItemsString: String abstract val noItemsString: String
abstract val switchString: String abstract val switchString: String
@ -116,36 +119,51 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
if (item.itemId == R.id.menu_context_delete) { if (item.itemId == R.id.menu_context_delete) {
pageViewModel.actions.offer(Action.UserClickedDeleteSelectedPages) pageViewModel.actions.tryEmit(Action.UserClickedDeleteSelectedPages)
return true return true
} }
pageViewModel.actions.offer(Action.ExitActionModeMenu) pageViewModel.actions.tryEmit(Action.ExitActionModeMenu)
return false return false
} }
override fun onDestroyActionMode(mode: ActionMode) { override fun onDestroyActionMode(mode: ActionMode) {
pageViewModel.actions.offer(Action.ExitActionModeMenu) pageViewModel.actions.tryEmit(Action.ExitActionModeMenu)
actionMode = null actionMode = null
} }
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
pageScreenState.value = pageScreenState.value.copy( pageScreenState.update {
searchQueryHint = searchQueryHint, copy(
searchQueryHint = this@PageFragment.searchQueryHint,
searchText = "", searchText = "",
searchValueChangedListener = { onTextChanged(it) }, searchValueChangedListener = { onTextChanged(it) },
clearSearchButtonClickListener = { onTextChanged("") }, clearSearchButtonClickListener = { onTextChanged("") },
screenTitle = screenTitle, screenTitle = this@PageFragment.screenTitle,
noItemsString = noItemsString, noItemsString = this@PageFragment.noItemsString,
switchString = switchString, switchString = this@PageFragment.switchString,
switchIsChecked = switchIsChecked, switchIsChecked = this@PageFragment.switchIsChecked,
onSwitchCheckedChanged = { onSwitchCheckedChanged(it).invoke() }, onSwitchCheckedChanged = { onSwitchChanged(it).invoke() },
deleteIconTitle = deleteIconTitle deleteIconTitle = this@PageFragment.deleteIconTitle
) )
}
val activity = requireActivity() as CoreMainActivity val activity = requireActivity() as CoreMainActivity
compositeDisposable.add(pageViewModel.effects.subscribe { it.invokeWith(activity) }) cancelCoroutineJobs()
pageViewModel.state.observe(viewLifecycleOwner, Observer(::render)) coroutineJobs.apply {
add(
pageViewModel
.effects
.onEach { it.invokeWith(activity) }
.launchIn(lifecycleScope)
)
add(
pageViewModel
.state
.onEach { render(it) }
.launchIn(lifecycleScope)
)
}
pageViewModel.alertDialogShower = alertDialogShower pageViewModel.alertDialogShower = alertDialogShower
} }
@ -168,9 +186,9 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
isSearchActive = pageScreenState.value.isSearchActive, isSearchActive = pageScreenState.value.isSearchActive,
onSearchClick = { onSearchClick = {
// Set the `isSearchActive` when the search button is clicked. // 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) DialogHost(alertDialogShower)
@ -186,8 +204,8 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
* @param searchText The current text entered in the search bar. * @param searchText The current text entered in the search bar.
*/ */
private fun onTextChanged(searchText: String) { private fun onTextChanged(searchText: String) {
pageScreenState.value = pageScreenState.value.copy(searchText = searchText) pageScreenState.update { copy(searchText = searchText) }
pageViewModel.actions.offer(Action.Filter(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. * @param isChecked The new checked state of the switch.
*/ */
private fun onSwitchCheckedChanged(isChecked: Boolean): () -> Unit = { private fun onSwitchChanged(isChecked: Boolean): () -> Unit = {
pageScreenState.value = pageScreenState.value.copy(switchIsChecked = isChecked) pageScreenState.update { copy(switchIsChecked = isChecked) }
pageViewModel.actions.offer(Action.UserClickedShowAllToggle(isChecked)) pageViewModel.actions.tryEmit(Action.UserClickedShowAllToggle(isChecked))
} }
/** /**
@ -209,7 +227,7 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
*/ */
private fun navigationIconClick(): () -> Unit = { private fun navigationIconClick(): () -> Unit = {
if (pageScreenState.value.isSearchActive) { if (pageScreenState.value.isSearchActive) {
pageScreenState.value = pageScreenState.value.copy(isSearchActive = false) pageScreenState.update { copy(isSearchActive = false) }
onTextChanged("") onTextChanged("")
} else { } else {
requireActivity().onBackPressedDispatcher.onBackPressed() requireActivity().onBackPressedDispatcher.onBackPressed()
@ -255,21 +273,20 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
compositeDisposable.clear() cancelCoroutineJobs()
}
private fun cancelCoroutineJobs() {
coroutineJobs.forEach {
it.cancel()
}
coroutineJobs.clear()
} }
private fun render(state: PageState<*>) { private fun render(state: PageState<*>) {
pageScreenState.value = pageScreenState.value.copy( pageScreenState.update {
switchIsEnabled = !state.isInSelectionState, copy(switchIsEnabled = !state.isInSelectionState, pageState = state)
// 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
)
if (state.isInSelectionState) { if (state.isInSelectionState) {
if (actionMode == null) { if (actionMode == null) {
actionMode = actionMode =
@ -282,9 +299,9 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
} }
override fun onItemClick(page: Page) { override fun onItemClick(page: Page) {
pageViewModel.actions.offer(Action.OnItemClick(page)) pageViewModel.actions.tryEmit(Action.OnItemClick(page))
} }
override fun onItemLongClick(page: Page): Boolean = 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, action: Action.UserClickedShowAllToggle,
state: BookmarkState state: BookmarkState
): BookmarkState { ): BookmarkState {
effects.offer(UpdateAllBookmarksPreference(sharedPreferenceUtil, action.isChecked)) effects.tryEmit(UpdateAllBookmarksPreference(sharedPreferenceUtil, action.isChecked))
return state.copy(showAll = 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 androidx.appcompat.app.AppCompatActivity
import io.reactivex.processors.PublishProcessor
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.PageDao import org.kiwix.kiwixmobile.core.dao.PageDao
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent 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 import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedBookmarks
data class ShowDeleteBookmarksDialog( data class ShowDeleteBookmarksDialog(
private val effects: PublishProcessor<SideEffect<*>>, private val effects: MutableSharedFlow<SideEffect<*>>,
private val state: PageState<LibkiwixBookmarkItem>, private val state: PageState<LibkiwixBookmarkItem>,
private val pageDao: PageDao, private val pageDao: PageDao,
private val viewModelScope: CoroutineScope, private val viewModelScope: CoroutineScope,
@ -42,7 +42,7 @@ data class ShowDeleteBookmarksDialog(
activity.cachedComponent.inject(this) activity.cachedComponent.inject(this)
dialogShower.show( dialogShower.show(
if (state.isInSelectionState) DeleteSelectedBookmarks else DeleteAllBookmarks, 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, action: Action.UserClickedShowAllToggle,
state: HistoryState state: HistoryState
): HistoryState { ): HistoryState {
effects.offer(UpdateAllHistoryPreference(sharedPreferenceUtil, action.isChecked)) effects.tryEmit(UpdateAllHistoryPreference(sharedPreferenceUtil, action.isChecked))
return state.copy(showAll = 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 androidx.appcompat.app.AppCompatActivity
import io.reactivex.processors.PublishProcessor
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.PageDao import org.kiwix.kiwixmobile.core.dao.PageDao
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent 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 import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedHistory
data class ShowDeleteHistoryDialog( data class ShowDeleteHistoryDialog(
private val effects: PublishProcessor<SideEffect<*>>, private val effects: MutableSharedFlow<SideEffect<*>>,
private val state: HistoryState, private val state: HistoryState,
private val pageDao: PageDao, private val pageDao: PageDao,
private val viewModelScope: CoroutineScope, private val viewModelScope: CoroutineScope,
@ -40,7 +40,7 @@ data class ShowDeleteHistoryDialog(
override fun invokeWith(activity: AppCompatActivity) { override fun invokeWith(activity: AppCompatActivity) {
activity.cachedComponent.inject(this) activity.cachedComponent.inject(this)
dialogShower.show(if (state.isInSelectionState) DeleteSelectedHistory else DeleteAllHistory, { 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, action: Action.UserClickedShowAllToggle,
state: NotesState state: NotesState
): NotesState { ): NotesState {
effects.offer(UpdateAllNotesPreference(sharedPreferenceUtil, action.isChecked)) effects.tryEmit(UpdateAllNotesPreference(sharedPreferenceUtil, action.isChecked))
return state.copy(showAll = 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 androidx.appcompat.app.AppCompatActivity
import io.reactivex.processors.PublishProcessor
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.PageDao import org.kiwix.kiwixmobile.core.dao.PageDao
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent 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 import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedNotes
data class ShowDeleteNotesDialog( data class ShowDeleteNotesDialog(
private val effects: PublishProcessor<SideEffect<*>>, private val effects: MutableSharedFlow<SideEffect<*>>,
private val state: NotesState, private val state: NotesState,
private val pageDao: PageDao, private val pageDao: PageDao,
private val viewModelScope: CoroutineScope, private val viewModelScope: CoroutineScope,
@ -42,7 +42,7 @@ data class ShowDeleteNotesDialog(
dialogShower.show( dialogShower.show(
if (state.isInSelectionState) DeleteSelectedNotes else DeleteAllNotes, 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 package org.kiwix.kiwixmobile.core.page.notes.viewmodel.effects
import androidx.appcompat.app.AppCompatActivity 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.base.SideEffect
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent
import org.kiwix.kiwixmobile.core.page.adapter.Page 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 import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.ShowNoteDialog
data class ShowOpenNoteDialog( data class ShowOpenNoteDialog(
private val effects: PublishProcessor<SideEffect<*>>, private val effects: MutableSharedFlow<SideEffect<*>>,
private val page: Page, private val page: Page,
private val zimReaderContainer: ZimReaderContainer, private val zimReaderContainer: ZimReaderContainer,
private val dialogShower: DialogShower private val dialogShower: DialogShower
@ -40,10 +40,10 @@ data class ShowOpenNoteDialog(
activity.cachedComponent.inject(this) activity.cachedComponent.inject(this)
dialogShower.show( dialogShower.show(
ShowNoteDialog, ShowNoteDialog,
{ effects.offer(OpenPage(page, zimReaderContainer)) }, { effects.tryEmit(OpenPage(page, zimReaderContainer)) },
{ {
val item = page as NoteListItem 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.Page
import org.kiwix.kiwixmobile.core.page.adapter.PageRelated import org.kiwix.kiwixmobile.core.page.adapter.PageRelated
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem 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 class PageState<T : Page> {
abstract val pageItems: List<T> 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 currentItemIdentifier = if (it is LibkiwixBookmarkItem) it.url else it.id
val pageIdentifier = if (it is LibkiwixBookmarkItem) page.url else page.id val pageIdentifier = if (it is LibkiwixBookmarkItem) page.url else page.id
if (currentItemIdentifier == pageIdentifier) { if (currentItemIdentifier == pageIdentifier) {
it.apply { when (it) {
isSelected = !isSelected 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 { } else {
it it

View File

@ -18,14 +18,21 @@
package org.kiwix.kiwixmobile.core.page.viewmodel package org.kiwix.kiwixmobile.core.page.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineDispatcher
import io.reactivex.disposables.Disposable
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope 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.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.PageDao import org.kiwix.kiwixmobile.core.dao.PageDao
import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.adapter.Page
@ -54,32 +61,32 @@ abstract class PageViewModel<T : Page, S : PageState<T>>(
lateinit var alertDialogShower: AlertDialogShower lateinit var alertDialogShower: AlertDialogShower
private lateinit var pageViewModelClickListener: PageViewModelClickListener 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 { @VisibleForTesting
MutableLiveData<S>().apply { fun getMutableStateForTestCases() = _state
value = initialState()
}
}
private val compositeDisposable = CompositeDisposable()
val effects = PublishProcessor.create<SideEffect<*>>()
val actions = PublishProcessor.create<Action>()
init { init {
addDisposablesToCompositeDisposable() coroutineJobs.apply {
add(observeActions())
add(observePages())
}
} }
private fun viewStateReducer(): Disposable = private fun observeActions() =
actions.map { state.value?.let { value -> reduce(it, value) } } actions.map { action -> reduce(action, state.value) }
.subscribe(state::postValue, Throwable::printStackTrace) .onEach { newState -> _state.value = newState }
.launchIn(viewModelScope)
protected fun addDisposablesToCompositeDisposable() { private fun observePages(dispatcher: CoroutineDispatcher = Dispatchers.IO) =
compositeDisposable.addAll( pageDao.pages()
viewStateReducer(), .flowOn(dispatcher)
pageDao.pages().subscribeOn(Schedulers.io()) .onEach { actions.tryEmit(UpdatePages(it)) }
.subscribe({ actions.offer(UpdatePages(it)) }, Throwable::printStackTrace) .launchIn(viewModelScope)
)
}
private fun reduce(action: Action, state: S): S = private fun reduce(action: Action, state: S): S =
when (action) { when (action) {
@ -103,7 +110,7 @@ abstract class PageViewModel<T : Page, S : PageState<T>>(
): S ): S
private fun offerShowDeleteDialog(state: S): S { private fun offerShowDeleteDialog(state: S): S {
effects.offer(createDeletePageDialogEffect(state, viewModelScope = viewModelScope)) effects.tryEmit(createDeletePageDialogEffect(state, viewModelScope = viewModelScope))
return state return state
} }
@ -117,9 +124,9 @@ abstract class PageViewModel<T : Page, S : PageState<T>>(
return copyWithNewItems(state, state.getItemsAfterToggleSelectionOfItem(action.page)) return copyWithNewItems(state, state.getItemsAfterToggleSelectionOfItem(action.page))
} }
if (::pageViewModelClickListener.isInitialized) { if (::pageViewModelClickListener.isInitialized) {
effects.offer(pageViewModelClickListener.onItemClick(action.page)) effects.tryEmit(pageViewModelClickListener.onItemClick(action.page))
} else { } else {
effects.offer(OpenPage(action.page, zimReaderContainer)) effects.tryEmit(OpenPage(action.page, zimReaderContainer))
} }
return state return state
} }
@ -131,12 +138,15 @@ abstract class PageViewModel<T : Page, S : PageState<T>>(
abstract fun deselectAllPages(state: S): S abstract fun deselectAllPages(state: S): S
private fun exitFragment(state: S): S { private fun exitFragment(state: S): S {
effects.offer(PopFragmentBackstack) effects.tryEmit(PopFragmentBackstack)
return state return state
} }
override fun onCleared() { override fun onCleared() {
compositeDisposable.clear() coroutineJobs.forEach {
it.cancel()
}
coroutineJobs.clear()
super.onCleared() super.onCleared()
} }

View File

@ -21,11 +21,9 @@ package org.kiwix.kiwixmobile.core.page.bookmark.viewmodel
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.reactivex.Flowable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.reactive.asPublisher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach 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.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower 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.InstantExecutorExtension
import java.util.UUID import java.util.UUID
@ -67,9 +66,7 @@ internal class BookmarkViewModelTest {
every { zimReaderContainer.name } returns "zimName" every { zimReaderContainer.name } returns "zimName"
every { sharedPreferenceUtil.showBookmarksAllBooks } returns true every { sharedPreferenceUtil.showBookmarksAllBooks } returns true
every { libkiwixBookMarks.bookmarks() } returns itemsFromDb every { libkiwixBookMarks.bookmarks() } returns itemsFromDb
every { libkiwixBookMarks.pages() } returns Flowable.fromPublisher( every { libkiwixBookMarks.pages() } returns libkiwixBookMarks.bookmarks()
libkiwixBookMarks.bookmarks().asPublisher()
)
viewModel = viewModel =
BookmarkViewModel(libkiwixBookMarks, zimReaderContainer, sharedPreferenceUtil).apply { BookmarkViewModel(libkiwixBookMarks, zimReaderContainer, sharedPreferenceUtil).apply {
alertDialogShower = dialogShower alertDialogShower = dialogShower
@ -106,13 +103,24 @@ internal class BookmarkViewModelTest {
} }
@Test @Test
fun `offerUpdateToShowAllToggle offers UpdateAllBookmarksPreference`() { fun `offerUpdateToShowAllToggle offers UpdateAllBookmarksPreference`() = runTest {
viewModel.effects.test().also { testFlow(
flow = viewModel.effects,
triggerAction = {
viewModel.offerUpdateToShowAllToggle( viewModel.offerUpdateToShowAllToggle(
Action.UserClickedShowAllToggle(false), Action.UserClickedShowAllToggle(false),
bookmarkState() bookmarkState()
) )
}.assertValues(UpdateAllBookmarksPreference(sharedPreferenceUtil, false)) },
assert = {
assertThat(awaitItem()).isEqualTo(
UpdateAllBookmarksPreference(
sharedPreferenceUtil,
false
)
)
}
)
} }
@Test @Test

View File

@ -22,9 +22,9 @@ import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
import io.mockk.verify import io.mockk.verify
import io.reactivex.processors.PublishProcessor
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.base.SideEffect
@ -40,7 +40,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedBookmar
import java.util.UUID import java.util.UUID
internal class ShowDeleteBookmarksDialogTest { internal class ShowDeleteBookmarksDialogTest {
val effects = mockk<PublishProcessor<SideEffect<*>>>(relaxed = true) val effects = mockk<MutableSharedFlow<SideEffect<*>>>(relaxed = true)
private val newBookmarksDao = mockk<NewBookmarksDao>() private val newBookmarksDao = mockk<NewBookmarksDao>()
val activity = mockk<CoreMainActivity>() val activity = mockk<CoreMainActivity>()
private val dialogShower = mockk<DialogShower>(relaxed = true) private val dialogShower = mockk<DialogShower>(relaxed = true)
@ -61,7 +61,7 @@ internal class ShowDeleteBookmarksDialogTest {
showDeleteBookmarksDialog.invokeWith(activity) showDeleteBookmarksDialog.invokeWith(activity)
verify { dialogShower.show(any(), capture(lambdaSlot)) } verify { dialogShower.show(any(), capture(lambdaSlot)) }
lambdaSlot.captured.invoke() lambdaSlot.captured.invoke()
verify { effects.offer(DeletePageItems(bookmarkState(), newBookmarksDao, viewModelScope)) } verify { effects.tryEmit(DeletePageItems(bookmarkState(), newBookmarksDao, viewModelScope)) }
} }
private fun mockkActivityInjection(showDeleteBookmarksDialog: ShowDeleteBookmarksDialog) { 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.clearAllMocks
import io.mockk.every import io.mockk.every
import io.mockk.mockk 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach 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.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower 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.InstantExecutorExtension
import org.kiwix.sharedFunctions.setScheduler
@ExtendWith(InstantExecutorExtension::class) @ExtendWith(InstantExecutorExtension::class)
internal class HistoryViewModelTest { internal class HistoryViewModelTest {
@ -39,16 +36,10 @@ internal class HistoryViewModelTest {
private val viewModelScope = CoroutineScope(Dispatchers.IO) private val viewModelScope = CoroutineScope(Dispatchers.IO)
private lateinit var viewModel: HistoryViewModel private lateinit var viewModel: HistoryViewModel
private val testScheduler = TestScheduler()
private val zimReaderSource: ZimReaderSource = mockk() private val zimReaderSource: ZimReaderSource = mockk()
init { private val itemsFromDb: MutableSharedFlow<List<Page>> =
setScheduler(testScheduler) MutableSharedFlow<List<Page>>(0)
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
}
private val itemsFromDb: PublishProcessor<List<Page>> =
PublishProcessor.create()
@BeforeEach @BeforeEach
fun init() { fun init() {
@ -89,13 +80,21 @@ internal class HistoryViewModelTest {
} }
@Test @Test
fun `offerUpdateToShowAllToggle offers UpdateAllHistoryPreference`() { fun `offerUpdateToShowAllToggle offers UpdateAllHistoryPreference`() = runTest {
viewModel.effects.test().also { testFlow(
flow = viewModel.effects,
triggerAction = {
viewModel.offerUpdateToShowAllToggle( viewModel.offerUpdateToShowAllToggle(
UserClickedShowAllToggle(false), UserClickedShowAllToggle(false),
historyState() historyState()
) )
}.assertValues(UpdateAllHistoryPreference(sharedPreferenceUtil, false)) },
assert = {
assertThat(awaitItem()).isEqualTo(
UpdateAllHistoryPreference(sharedPreferenceUtil, false)
)
}
)
} }
@Test @Test

View File

@ -4,9 +4,9 @@ import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
import io.mockk.verify import io.mockk.verify
import io.reactivex.processors.PublishProcessor
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.base.SideEffect 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 import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedHistory
internal class ShowDeleteHistoryDialogTest { internal class ShowDeleteHistoryDialogTest {
val effects = mockk<PublishProcessor<SideEffect<*>>>(relaxed = true) val effects = mockk<MutableSharedFlow<SideEffect<*>>>(relaxed = true)
private val historyDao = mockk<HistoryDao>() private val historyDao = mockk<HistoryDao>()
val activity = mockk<CoreMainActivity>() val activity = mockk<CoreMainActivity>()
private val dialogShower = mockk<DialogShower>(relaxed = true) private val dialogShower = mockk<DialogShower>(relaxed = true)
@ -42,7 +42,7 @@ internal class ShowDeleteHistoryDialogTest {
showDeleteHistoryDialog.invokeWith(activity) showDeleteHistoryDialog.invokeWith(activity)
verify { dialogShower.show(any(), capture(lambdaSlot)) } verify { dialogShower.show(any(), capture(lambdaSlot)) }
lambdaSlot.captured.invoke() lambdaSlot.captured.invoke()
verify { effects.offer(DeletePageItems(historyState(), historyDao, viewModelScope)) } verify { effects.tryEmit(DeletePageItems(historyState(), historyDao, viewModelScope)) }
} }
@Test @Test

View File

@ -18,18 +18,16 @@
package org.kiwix.kiwixmobile.core.page.viewmodel package org.kiwix.kiwixmobile.core.page.viewmodel
import com.jraska.livedata.test
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.every import io.mockk.every
import io.mockk.mockk 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.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach 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.PageImpl
import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.pageState 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.Exit
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.ExitActionModeMenu 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.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.Action.UpdatePages
import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UserClickedDeleteButton 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.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.page.viewmodel.effects.OpenPage
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.PopFragmentBackstack import org.kiwix.kiwixmobile.core.search.viewmodel.effects.PopFragmentBackstack
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.files.testFlow
import org.kiwix.sharedFunctions.InstantExecutorExtension import org.kiwix.sharedFunctions.InstantExecutorExtension
import org.kiwix.sharedFunctions.setScheduler
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(InstantExecutorExtension::class) @ExtendWith(InstantExecutorExtension::class)
@ -65,14 +63,8 @@ internal class PageViewModelTest {
private val sharedPreferenceUtil: SharedPreferenceUtil = mockk() private val sharedPreferenceUtil: SharedPreferenceUtil = mockk()
private lateinit var viewModel: TestablePageViewModel private lateinit var viewModel: TestablePageViewModel
private val testScheduler = TestScheduler() private val itemsFromDb: MutableSharedFlow<List<Page>> =
private val itemsFromDb: PublishProcessor<List<Page>> = MutableSharedFlow<List<Page>>(0)
PublishProcessor.create()
init {
setScheduler(testScheduler)
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
}
@BeforeEach @BeforeEach
fun init() { fun init() {
@ -91,73 +83,139 @@ internal class PageViewModelTest {
} }
@Test @Test
fun `initial state is Initialising`() { fun `initial state is Initialising`() = runTest {
viewModel.state.test().assertValue(pageState()) testFlow(
flow = viewModel.state,
triggerAction = {},
assert = { assertThat(awaitItem()).isEqualTo(pageState()) }
)
} }
@Test @Test
fun `Exit calls PopFragmentBackstack`() { fun `Exit calls PopFragmentBackstack`() = runTest {
viewModel.effects.test().also { viewModel.actions.offer(Exit) } testFlow(
.assertValue(PopFragmentBackstack) flow = viewModel.effects,
viewModel.state.test().assertValue(pageState()) triggerAction = { viewModel.actions.tryEmit(Exit) },
assert = { assertThat(awaitItem()).isEqualTo(PopFragmentBackstack) }
)
testFlow(
flow = viewModel.state,
triggerAction = {},
assert = { assertThat(awaitItem()).isEqualTo(pageState()) }
)
} }
@Test @Test
fun `ExitActionModeMenu calls deslectAllPages`() { fun `ExitActionModeMenu calls deslectAllPages`() = runTest {
viewModel.actions.offer(ExitActionModeMenu) testFlow(
viewModel.state.test().assertValue(TestablePageState(searchTerm = "deselectAllPagesCalled")) flow = viewModel.state,
triggerAction = { viewModel.actions.tryEmit(ExitActionModeMenu) },
assert = {
assertThat(awaitItem()).isEqualTo(TestablePageState(searchTerm = ""))
assertThat(awaitItem())
.isEqualTo(TestablePageState(searchTerm = "deselectAllPagesCalled"))
}
)
} }
@Test @Test
fun `UserClickedShowAllToggle calls offerUpdateToShowAllToggle`() { fun `UserClickedShowAllToggle calls offerUpdateToShowAllToggle`() = runTest {
val action = UserClickedShowAllToggle(true) testFlow(
viewModel.actions.offer(action) flow = viewModel.state,
viewModel.state.test() triggerAction = {
.assertValue(TestablePageState(searchTerm = "offerUpdateToShowAllToggleCalled")) viewModel.actions.tryEmit(UserClickedShowAllToggle(true))
},
assert = {
assertThat(awaitItem()).isEqualTo(TestablePageState(searchTerm = ""))
assertThat(awaitItem())
.isEqualTo(TestablePageState(searchTerm = "offerUpdateToShowAllToggleCalled"))
}
)
} }
@Test @Test
fun `UserClickedDeleteButton calls createDeletePageDialogEffect`() { fun `UserClickedDeleteButton calls createDeletePageDialogEffect`() = runTest {
viewModel.actions.offer(UserClickedDeleteButton) viewModel.actions.tryEmit(UserClickedDeleteButton)
advanceUntilIdle()
assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true) assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true)
} }
@Test @Test
fun `UserClickedDeleteSelectedPages calls createDeletePageDialogEffect`() { fun `UserClickedDeleteSelectedPages calls createDeletePageDialogEffect`() = runTest {
viewModel.actions.offer(UserClickedDeleteSelectedPages) viewModel.actions.tryEmit(UserClickedDeleteSelectedPages)
advanceUntilIdle()
assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true) assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true)
} }
@Test @Test
internal fun `OnItemClick selects item if one is selected`() { internal fun `OnItemClick selects item if one is selected`() = runTest {
val zimReaderSource: ZimReaderSource = mockk() val zimReaderSource: ZimReaderSource = mockk()
testFlow(
viewModel.state,
triggerAction = {
val page = PageImpl(isSelected = true, zimReaderSource = zimReaderSource) val page = PageImpl(isSelected = true, zimReaderSource = zimReaderSource)
viewModel.state.postValue(TestablePageState(listOf(page))) viewModel.getMutableStateForTestCases().value = TestablePageState(listOf(page))
viewModel.actions.offer(OnItemClick(page)) viewModel.actions.tryEmit(OnItemClick(page))
viewModel.state.test() },
.assertValue(TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))) assert = {
assertThat(awaitItem()).isEqualTo(TestablePageState())
assertThat(awaitItem())
.isEqualTo(
TestablePageState(
listOf(PageImpl(zimReaderSource = zimReaderSource))
)
)
}
)
} }
@Test @Test
internal fun `OnItemClick offers OpenPage if none is selected`() { internal fun `OnItemClick offers OpenPage if none is selected`() = runTest {
val zimReaderSource: ZimReaderSource = mockk() val zimReaderSource: ZimReaderSource = mockk()
viewModel.state.postValue( 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))) TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))
) )
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 @Test
internal fun `OnItemLongClick selects item if none is selected`() { internal fun `OnItemLongClick selects item if none is selected`() = runTest {
val zimReaderSource: ZimReaderSource = mockk() val zimReaderSource: ZimReaderSource = mockk()
val page = PageImpl(zimReaderSource = zimReaderSource) val page = PageImpl(zimReaderSource = zimReaderSource)
viewModel.state.postValue(TestablePageState(listOf(page))) testFlow(
viewModel.actions.offer(OnItemLongClick(page)) viewModel.state,
viewModel.state.test().assertValue( triggerAction = {
viewModel.getMutableStateForTestCases().value = TestablePageState(listOf(page))
viewModel.actions.tryEmit(OnItemLongClick(page))
},
assert = {
assertThat(awaitItem()).isEqualTo(TestablePageState())
assertThat(awaitItem()).isEqualTo(
TestablePageState( TestablePageState(
listOf( listOf(
PageImpl( PageImpl(
@ -168,18 +226,30 @@ internal class PageViewModelTest {
) )
) )
} }
)
@Test
fun `Filter calls updatePagesBasedOnFilter`() {
viewModel.actions.offer(Filter("Called"))
viewModel.state.test()
.assertValue(TestablePageState(searchTerm = "updatePagesBasedOnFilterCalled"))
} }
@Test @Test
fun `UpdatePages calls updatePages`() { fun `Filter calls updatePagesBasedOnFilter`() = runTest {
viewModel.actions.offer(UpdatePages(emptyList())) testFlow(
viewModel.state.test() viewModel.state,
.assertValue(TestablePageState(searchTerm = "updatePagesCalled")) 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.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope 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.BeforeEach
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.kiwix.sharedFunctions.resetSchedulers
import org.kiwix.sharedFunctions.setScheduler
import java.io.File import java.io.File
class FileSearchTest { class FileSearchTest {
@ -54,10 +51,6 @@ class FileSearchTest {
private val storageDevice: StorageDevice = mockk() private val storageDevice: StorageDevice = mockk()
private val scanningProgressListener: ScanningProgressListener = mockk() private val scanningProgressListener: ScanningProgressListener = mockk()
init {
setScheduler(Schedulers.trampoline())
}
@BeforeEach @BeforeEach
fun init() { fun init() {
clearMocks(context, externalStorageDirectory, contentResolver, storageDevice) clearMocks(context, externalStorageDirectory, contentResolver, storageDevice)
@ -78,7 +71,6 @@ class FileSearchTest {
@AfterAll @AfterAll
fun teardown() { fun teardown() {
deleteTempDirectory() deleteTempDirectory()
resetSchedulers()
} }
@Nested @Nested