Migrate RomsRepository and RomListViewModel to Kotlin coroutines

This also fixes the slowdown when performing a ROM search when a large set of ROMs is present since the filtering is now performed outside of the UI thread
This commit is contained in:
Rafael Caetano 2022-08-20 18:59:01 +01:00
parent 04dcd3e73a
commit 8ee5c80be8
9 changed files with 205 additions and 201 deletions

View File

@ -128,6 +128,7 @@ dependencies {
implementation(flexbox)
implementation(gson)
implementation(hilt)
implementation(kotlinxCoroutinesRx)
implementation(picasso)
implementation(markwon)
implementation(markwonImagePicasso)

View File

@ -1,18 +1,18 @@
package me.magnum.melonds.domain.repositories
import android.net.Uri
import io.reactivex.Maybe
import io.reactivex.Observable
import kotlinx.coroutines.flow.Flow
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.RomScanningStatus
import java.util.*
interface RomsRepository {
fun getRoms(): Observable<List<Rom>>
fun getRoms(): Flow<List<Rom>>
fun getRomScanningStatus(): Observable<RomScanningStatus>
fun getRomAtPath(path: String): Maybe<Rom>
fun getRomAtUri(uri: Uri): Maybe<Rom>
suspend fun getRomAtPath(path: String): Rom?
suspend fun getRomAtUri(uri: Uri): Rom?
fun updateRomConfig(rom: Rom, romConfig: RomConfig)
fun setRomLastPlayed(rom: Rom, lastPlayed: Date)

View File

@ -6,13 +6,15 @@ import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import io.reactivex.*
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.BehaviorSubject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.magnum.melonds.common.romprocessors.RomFileProcessorFactory
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
@ -26,7 +28,7 @@ import java.io.FileReader
import java.io.OutputStreamWriter
import java.lang.reflect.Type
import java.util.*
import kotlin.collections.ArrayList
import java.util.concurrent.atomic.AtomicBoolean
class FileSystemRomsRepository(
private val context: Context,
@ -40,17 +42,21 @@ class FileSystemRomsRepository(
private const val ROM_DATA_FILE = "rom_data.json"
}
private val coroutineScope = CoroutineScope(Dispatchers.Main)
private val disposables = CompositeDisposable()
private val romListType: Type = object : TypeToken<List<Rom>>(){}.type
private val romsSubject: BehaviorSubject<List<Rom>> = BehaviorSubject.create()
private val romsChannel: MutableSharedFlow<List<Rom>> = MutableSharedFlow(replay = 1, extraBufferCapacity = 0, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val scanningStatusSubject: BehaviorSubject<RomScanningStatus> = BehaviorSubject.createDefault(RomScanningStatus.NOT_SCANNING)
private val roms: ArrayList<Rom> = ArrayList()
private var areRomsLoaded = false
private var areRomsLoaded = AtomicBoolean(false)
init {
romsSubject.subscribeOn(Schedulers.io())
.subscribe { roms -> saveRomData(roms) }
.addTo(disposables)
coroutineScope.launch {
romsChannel.onEach {
saveRomData(it)
}.collect()
}
settingsRepository.observeRomSearchDirectories()
.subscribe { directories -> onRomSearchDirectoriesChanged(directories) }
.addTo(disposables)
@ -59,7 +65,7 @@ class FileSystemRomsRepository(
private fun onRomSearchDirectoriesChanged(searchDirectories: Array<Uri>) {
// If ROMs have not been loaded yet, there's no point in searching or discarding ROMs now.
// They will be scanned once needed
if (!areRomsLoaded)
if (!areRomsLoaded.get())
return
// TODO: Check if existing ROMs are still found in the new directory(s). How can we do that reliably using URIs?
@ -67,35 +73,30 @@ class FileSystemRomsRepository(
rescanRoms()
}
override fun getRoms(): Observable<List<Rom>> {
if (!areRomsLoaded) {
areRomsLoaded = true
loadCachedRoms()
override fun getRoms(): Flow<List<Rom>> = flow {
if (areRomsLoaded.compareAndSet(false, true)) {
coroutineScope.launch {
loadCachedRoms()
}
}
return romsSubject
emitAll(romsChannel)
}
override fun getRomScanningStatus(): Observable<RomScanningStatus> {
return scanningStatusSubject
}
override fun getRomAtPath(path: String): Maybe<Rom> {
return getRoms().firstElement()
.flatMap {
it.find { rom ->
val romPath = FileUtils.getAbsolutePathFromSAFUri(context, rom.uri)
romPath == path
}?.let { rom -> Maybe.just(rom) } ?: Maybe.empty()
}
override suspend fun getRomAtPath(path: String): Rom? {
return getRoms().first().find { rom ->
val romPath = FileUtils.getAbsolutePathFromSAFUri(context, rom.uri)
romPath == path
}
}
override fun getRomAtUri(uri: Uri): Maybe<Rom> {
return getRoms().firstElement()
.flatMap {
it.find { rom ->
rom.uri == uri
}?.let { rom -> Maybe.just(rom) } ?: Maybe.empty()
}
override suspend fun getRomAtUri(uri: Uri): Rom? {
return getRoms().first().find { rom ->
rom.uri == uri
}
}
override fun updateRomConfig(rom: Rom, romConfig: RomConfig) {
@ -118,28 +119,20 @@ class FileSystemRomsRepository(
}
override fun rescanRoms() {
scanForNewRoms()
.subscribeOn(Schedulers.io())
.subscribe(object : Observer<Rom> {
override fun onSubscribe(d: Disposable) {
scanningStatusSubject.onNext(RomScanningStatus.SCANNING)
}
coroutineScope.launch(Dispatchers.IO) {
scanningStatusSubject.onNext(RomScanningStatus.SCANNING)
override fun onNext(rom: Rom) {
addRom(rom)
}
scanForNewRoms().collect {
addRom(it)
}
override fun onError(e: Throwable) {}
override fun onComplete() {
scanningStatusSubject.onNext(RomScanningStatus.NOT_SCANNING)
}
})
scanningStatusSubject.onNext(RomScanningStatus.NOT_SCANNING)
}
}
override fun invalidateRoms() {
if (areRomsLoaded) {
if (areRomsLoaded.compareAndSet(true, false)) {
roms.clear()
areRomsLoaded = false
}
val cacheFile = File(context.filesDir, ROM_DATA_FILE)
@ -168,85 +161,60 @@ class FileSystemRomsRepository(
}
private fun onRomsChanged() {
romsSubject.onNext(ArrayList(roms))
romsChannel.tryEmit(roms)
}
private fun loadCachedRoms() {
getCachedRoms()
.filter { rom -> DocumentFile.fromSingleUri(context, rom.uri)?.exists() == true }
.toList()
.doOnSuccess { cachedRoms ->
roms.addAll(cachedRoms!!)
onRomsChanged()
}
.flatMapObservable { scanForNewRoms() }
.subscribeOn(Schedulers.io())
.subscribe(object : Observer<Rom> {
override fun onSubscribe(d: Disposable) {
scanningStatusSubject.onNext(RomScanningStatus.SCANNING)
}
private suspend fun loadCachedRoms() = withContext(Dispatchers.IO) {
scanningStatusSubject.onNext(RomScanningStatus.SCANNING)
override fun onNext(rom: Rom) {
addRom(rom)
}
val cachedRoms = getCachedRoms().filter {
DocumentFile.fromSingleUri(context, it.uri)?.exists() == true
}.toCollection(mutableListOf())
override fun onError(e: Throwable) {}
roms.addAll(cachedRoms)
onRomsChanged()
scanForNewRoms().collect {
addRom(it)
}
override fun onComplete() {
scanningStatusSubject.onNext(RomScanningStatus.NOT_SCANNING)
}
})
scanningStatusSubject.onNext(RomScanningStatus.NOT_SCANNING)
}
private fun scanForNewRoms(): Observable<Rom> {
return Observable.create(object : ObservableOnSubscribe<Rom> {
private fun findFiles(directory: DocumentFile, emitter: ObservableEmitter<Rom>) {
val files = directory.listFiles()
for (file in files) {
if (file.isDirectory) {
findFiles(file, emitter)
continue
}
romFileProcessorFactory.getFileRomProcessorForDocument(file)?.let { fileRomProcessor ->
fileRomProcessor.getRomFromUri(file.uri, directory.uri)?.let { emitter.onNext(it) }
}
}
private fun scanForNewRoms(): Flow<Rom> = flow {
for (directory in settingsRepository.getRomSearchDirectories()) {
val documentFile = DocumentFile.fromTreeUri(context, directory)
if (documentFile != null) {
findCachedRomFiles(documentFile, this)
}
override fun subscribe(emitter: ObservableEmitter<Rom>) {
for (directory in settingsRepository.getRomSearchDirectories()) {
val documentFile = DocumentFile.fromTreeUri(context, directory)
if (documentFile != null) {
findFiles(documentFile, emitter)
}
}
emitter.onComplete()
}
})
}
}
private fun getCachedRoms(): Observable<Rom> {
return Observable.create(ObservableOnSubscribe { emitter ->
val cacheFile = File(context.filesDir, ROM_DATA_FILE)
if (!cacheFile.isFile) {
emitter.onComplete()
return@ObservableOnSubscribe
private suspend fun findCachedRomFiles(directory: DocumentFile, collector: FlowCollector<Rom>) {
val files = directory.listFiles()
for (file in files) {
if (file.isDirectory) {
findCachedRomFiles(file, collector)
continue
}
try {
val roms = gson.fromJson<List<Rom>>(FileReader(cacheFile), romListType)
if (roms != null) {
for (rom in roms) {
emitter.onNext(rom)
}
}
emitter.onComplete()
} catch (_: Exception) {
emitter.onComplete()
romFileProcessorFactory.getFileRomProcessorForDocument(file)?.let { fileRomProcessor ->
fileRomProcessor.getRomFromUri(file.uri, directory.uri)?.let { collector.emit(it) }
}
})
}
}
private fun getCachedRoms(): Flow<Rom> = flow {
val cacheFile = File(context.filesDir, ROM_DATA_FILE)
if (!cacheFile.isFile) {
return@flow
}
try {
gson.fromJson<List<Rom>>(FileReader(cacheFile), romListType)?.forEach {
emit(it)
}
} catch (_: Exception) {
}
}
private fun saveRomData(romData: List<Rom>) {

View File

@ -11,6 +11,7 @@ import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import kotlinx.coroutines.rx2.rxMaybe
import me.magnum.melonds.common.Schedulers
import me.magnum.melonds.common.romprocessors.RomFileProcessorFactory
import me.magnum.melonds.common.uridelegates.UriHandler
@ -206,11 +207,15 @@ class EmulatorViewModel @Inject constructor(
}
fun getRomAtPath(path: String): Maybe<Rom> {
return romsRepository.getRomAtPath(path)
return rxMaybe {
romsRepository.getRomAtPath(path)
}
}
fun getRomAtUri(uri: Uri): Maybe<Rom> {
return romsRepository.getRomAtUri(uri)
return rxMaybe {
romsRepository.getRomAtUri(uri)
}
}
fun getEmulatorConfigurationForRom(rom: Rom): EmulatorConfiguration {

View File

@ -15,8 +15,10 @@ import androidx.appcompat.widget.SearchView
import androidx.core.content.getSystemService
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.Markwon
import kotlinx.coroutines.flow.collectLatest
import me.magnum.melonds.R
import me.magnum.melonds.common.Permission
import me.magnum.melonds.common.contracts.DirectoryPickerContract
@ -71,15 +73,22 @@ class RomListActivity : AppCompatActivity() {
val binding = ActivityRomListBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.hasRomScanningDirectories().observe(this) { hasDirectories ->
if (hasDirectories)
addRomListFragment()
else
addNoSearchDirectoriesFragment()
lifecycleScope.launchWhenStarted {
viewModel.hasSearchDirectories.collectLatest { hasDirectories ->
if (hasDirectories) {
addRomListFragment()
} else {
addNoSearchDirectoriesFragment()
}
}
}
viewModel.invalidDirectoryAccessEvent.observe(this) {
showInvalidDirectoryAccessDialog()
lifecycleScope.launchWhenStarted {
viewModel.invalidDirectoryAccessEvent.collectLatest {
showInvalidDirectoryAccessDialog()
}
}
updatesViewModel.getAppUpdate().observe(this) {
showUpdateAvailableDialog(it)
}

View File

@ -12,12 +12,14 @@ import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import io.reactivex.disposables.Disposable
import kotlinx.coroutines.flow.collectLatest
import me.magnum.melonds.R
import me.magnum.melonds.databinding.ItemRomConfigurableBinding
import me.magnum.melonds.databinding.ItemRomSimpleBinding
@ -26,8 +28,6 @@ import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomIconFiltering
import me.magnum.melonds.domain.model.RomScanningStatus
import me.magnum.melonds.ui.romlist.RomListFragment.RomListAdapter.RomViewHolder
import me.magnum.melonds.utils.FileUtils
import java.util.*
@AndroidEntryPoint
class RomListFragment : Fragment() {
@ -78,13 +78,18 @@ class RomListFragment : Fragment() {
adapter = romListAdapter
}
romListViewModel.getRomScanningStatus().observe(viewLifecycleOwner) { status ->
binding.swipeRefreshRoms.isRefreshing = status == RomScanningStatus.SCANNING
displayEmptyListViewIfRequired()
lifecycleScope.launchWhenStarted {
romListViewModel.romScanningStatus.collectLatest { status ->
binding.swipeRefreshRoms.isRefreshing = status == RomScanningStatus.SCANNING
displayEmptyListViewIfRequired()
}
}
romListViewModel.getRoms().observe(viewLifecycleOwner) { roms ->
romListAdapter.setRoms(roms)
displayEmptyListViewIfRequired()
lifecycleScope.launchWhenStarted {
romListViewModel.roms.collectLatest { roms ->
romListAdapter.setRoms(roms)
displayEmptyListViewIfRequired()
}
}
}

View File

@ -1,13 +1,15 @@
package me.magnum.melonds.ui.romlist
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import me.magnum.melonds.common.DirectoryAccessValidator
import me.magnum.melonds.common.Permission
import me.magnum.melonds.common.Schedulers
@ -20,7 +22,8 @@ import me.magnum.melonds.domain.repositories.SettingsRepository
import me.magnum.melonds.domain.services.ConfigurationDirectoryVerifier
import me.magnum.melonds.extensions.addTo
import me.magnum.melonds.impl.RomIconProvider
import me.magnum.melonds.utils.SingleLiveEvent
import me.magnum.melonds.utils.EventSharedFlow
import me.magnum.melonds.utils.SubjectSharedFlow
import java.text.Normalizer
import java.util.*
import javax.inject.Inject
@ -40,70 +43,67 @@ class RomListViewModel @Inject constructor(
private val disposables: CompositeDisposable = CompositeDisposable()
private val _invalidDirectoryAccessEvent = SingleLiveEvent<Unit>()
val invalidDirectoryAccessEvent: LiveData<Unit> = _invalidDirectoryAccessEvent
private val _searchQuery = MutableStateFlow("")
private val _sortingMode = MutableStateFlow(settingsRepository.getRomSortingMode())
private val _sortingOrder = MutableStateFlow(settingsRepository.getRomSortingOrder())
private val romsLiveData = MutableLiveData<List<Rom>>()
private val hasSearchDirectoriesLiveData = MutableLiveData<Boolean>()
private val romsFilteredLiveData: MediatorLiveData<List<Rom>>
private val _hasSearchDirectories = SubjectSharedFlow<Boolean>()
val hasSearchDirectories: Flow<Boolean> = _hasSearchDirectories
private var romSearchQuery = ""
private var sortingMode = settingsRepository.getRomSortingMode()
private var sortingOrder = settingsRepository.getRomSortingOrder()
private val _invalidDirectoryAccessEvent = EventSharedFlow<Unit>()
val invalidDirectoryAccessEvent: Flow<Unit> = _invalidDirectoryAccessEvent
private val _romScanningStatus = MutableStateFlow(RomScanningStatus.NOT_SCANNING)
val romScanningStatus = _romScanningStatus.asStateFlow()
private val _roms = MutableStateFlow<List<Rom>>(emptyList())
val roms = _roms.asStateFlow()
init {
settingsRepository.observeRomIconFiltering()
.subscribe { romsLiveData.postValue(romsLiveData.value) }
.addTo(disposables)
settingsRepository.observeRomSearchDirectories()
.startWith(settingsRepository.getRomSearchDirectories())
.distinctUntilChanged()
.subscribe { directories -> hasSearchDirectoriesLiveData.postValue(directories.isNotEmpty()) }
.subscribe { directories -> _hasSearchDirectories.tryEmit(directories.isNotEmpty()) }
.addTo(disposables)
romsFilteredLiveData = MediatorLiveData<List<Rom>>().apply {
addSource(romsLiveData) {
val romList = if (romSearchQuery.isEmpty()) {
it
} else {
it?.filter { rom ->
settingsRepository.observeRomIconFiltering()
.subscribe { _roms.value = _roms.value }
.addTo(disposables)
romsRepository.getRomScanningStatus()
.subscribe { status -> _romScanningStatus.value = status }
.addTo(disposables)
combine(romsRepository.getRoms(), _searchQuery) { roms, query ->
val romList = if (query.isEmpty()) {
roms
} else {
withContext(Dispatchers.Default) {
roms.filter { rom ->
if (!isActive) {
return@withContext emptyList()
}
val normalizedName = Normalizer.normalize(rom.name, Normalizer.Form.NFD).replace("[^\\p{ASCII}]", "")
val normalizedPath = Normalizer.normalize(uriHandler.getUriDocument(rom.uri)?.name, Normalizer.Form.NFD).replace("[^\\p{ASCII}]", "")
normalizedName.contains(romSearchQuery, true) || normalizedPath.contains(romSearchQuery, true)
}
}
if (romList != null) {
value = when (sortingMode) {
SortingMode.ALPHABETICALLY -> romList.sortedWith(buildAlphabeticalRomComparator())
SortingMode.RECENTLY_PLAYED -> romList.sortedWith(buildRecentlyPlayedRomComparator())
normalizedName.contains(query, true) || normalizedPath.contains(query, true)
}
}
}
}
romsRepository.getRoms()
.subscribeOn(schedulers.backgroundThreadScheduler)
.subscribe { roms -> romsLiveData.postValue(roms) }
.addTo(disposables)
}
_roms.value = when (_sortingMode.value) {
SortingMode.ALPHABETICALLY -> romList.sortedWith(buildAlphabeticalRomComparator(_sortingOrder.value))
SortingMode.RECENTLY_PLAYED -> romList.sortedWith(buildRecentlyPlayedRomComparator(_sortingOrder.value))
}
}.launchIn(viewModelScope)
fun hasRomScanningDirectories(): LiveData<Boolean> {
return hasSearchDirectoriesLiveData
}
fun getRoms(): LiveData<List<Rom>> {
return romsFilteredLiveData
}
fun getRomScanningStatus(): LiveData<RomScanningStatus> {
val scanningStatusLiveData = MutableLiveData<RomScanningStatus>()
val disposable = romsRepository.getRomScanningStatus()
.subscribe { status -> scanningStatusLiveData.postValue(status) }
disposables.add(disposable)
return scanningStatusLiveData
combine(_sortingMode, _sortingOrder) { sortingMode, sortingOrder ->
_roms.value = when (sortingMode) {
SortingMode.ALPHABETICALLY -> _roms.value.sortedWith(buildAlphabeticalRomComparator(sortingOrder))
SortingMode.RECENTLY_PLAYED -> _roms.value.sortedWith(buildRecentlyPlayedRomComparator(sortingOrder))
}
}.launchIn(viewModelScope)
}
fun refreshRoms() {
@ -129,27 +129,25 @@ class RomListViewModel @Inject constructor(
}
fun setRomSearchQuery(query: String?) {
romSearchQuery = Normalizer.normalize(query ?: "", Normalizer.Form.NFD).replace("[^\\p{ASCII}]", "")
romsLiveData.value = romsLiveData.value
_searchQuery.tryEmit(Normalizer.normalize(query ?: "", Normalizer.Form.NFD).replace("[^\\p{ASCII}]", ""))
}
fun setRomSorting(sortingMode: SortingMode) {
if (sortingMode == this.sortingMode) {
sortingOrder = if (sortingOrder == SortingOrder.ASCENDING)
if (sortingMode == _sortingMode.value) {
val newSortingOrder = if (_sortingOrder.value == SortingOrder.ASCENDING)
SortingOrder.DESCENDING
else
SortingOrder.ASCENDING
settingsRepository.setRomSortingOrder(sortingOrder)
settingsRepository.setRomSortingOrder(_sortingOrder.value)
_sortingOrder.value = newSortingOrder
} else {
this.sortingMode = sortingMode
sortingOrder = sortingMode.defaultOrder
settingsRepository.setRomSortingMode(sortingMode)
settingsRepository.setRomSortingOrder(sortingOrder)
}
settingsRepository.setRomSortingOrder(sortingMode.defaultOrder)
romsLiveData.value = romsLiveData.value
_sortingMode.value = sortingMode
_sortingOrder.value = sortingMode.defaultOrder
}
}
fun getConsoleConfigurationDirResult(consoleType: ConsoleType): ConfigurationDirResult {
@ -173,7 +171,7 @@ class RomListViewModel @Inject constructor(
uriPermissionManager.persistDirectoryPermissions(directoryUri, Permission.READ_WRITE)
settingsRepository.addRomSearchDirectory(directoryUri)
} else {
_invalidDirectoryAccessEvent.call()
_invalidDirectoryAccessEvent.tryEmit(Unit)
}
}
@ -190,7 +188,7 @@ class RomListViewModel @Inject constructor(
settingsRepository.setDsBiosDirectory(uri)
true
} else {
_invalidDirectoryAccessEvent.call()
_invalidDirectoryAccessEvent.tryEmit(Unit)
false
}
}
@ -208,7 +206,7 @@ class RomListViewModel @Inject constructor(
settingsRepository.setDsiBiosDirectory(uri)
true
} else {
_invalidDirectoryAccessEvent.call()
_invalidDirectoryAccessEvent.tryEmit(Unit)
false
}
}
@ -229,7 +227,7 @@ class RomListViewModel @Inject constructor(
return uriHandler.getUriDocument(uri)?.name
}
private fun buildAlphabeticalRomComparator(): Comparator<Rom> {
private fun buildAlphabeticalRomComparator(sortingOrder: SortingOrder): Comparator<Rom> {
return if (sortingOrder == SortingOrder.ASCENDING) {
Comparator { o1: Rom, o2: Rom ->
o1.name.compareTo(o2.name)
@ -241,7 +239,7 @@ class RomListViewModel @Inject constructor(
}
}
private fun buildRecentlyPlayedRomComparator(): Comparator<Rom> {
private fun buildRecentlyPlayedRomComparator(sortingOrder: SortingOrder): Comparator<Rom> {
return if (sortingOrder == SortingOrder.ASCENDING) {
Comparator { o1: Rom, o2: Rom ->
when {

View File

@ -0,0 +1,16 @@
package me.magnum.melonds.utils
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
/**
* Creates a [MutableSharedFlow] that holds a single value and has no initial value.
*/
@Suppress("FunctionName", "UNCHECKED_CAST")
fun <T> SubjectSharedFlow() = MutableSharedFlow<T>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
/**
* Creates a [MutableSharedFlow] that doesn't hold any value. Suitable to create flows used to fire events.
*/
@Suppress("FunctionName", "UNCHECKED_CAST")
fun <T> EventSharedFlow() = MutableSharedFlow<T>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

View File

@ -16,6 +16,7 @@ object Dependencies {
const val Hilt = "2.38.1"
const val Junit = "4.12"
const val Kotlin = "1.5.10"
const val KotlinxCoroutinesRx = "1.6.4"
const val LifecycleExtensions = "2.0.0"
const val LifecycleViewModel = "2.3.1"
const val MasterSwitchPreference = "0.9.4"
@ -75,6 +76,7 @@ object Dependencies {
const val flexbox = "com.google.android.flexbox:flexbox:${Versions.Flexbox}"
const val gson = "com.google.code.gson:gson:${Versions.Gson}"
const val hilt = "com.google.dagger:hilt-android:${Versions.Hilt}"
const val kotlinxCoroutinesRx = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:${Versions.KotlinxCoroutinesRx}"
const val picasso = "com.squareup.picasso:picasso:${Versions.Picasso}"
const val markwon = "io.noties.markwon:core:${Versions.Markwon}"
const val markwonImagePicasso = "io.noties.markwon:image-picasso:${Versions.Markwon}"