Convert cheats screen logic from Rx to Coroutines

This commit is contained in:
Rafael Caetano 2023-11-10 00:50:40 +00:00
parent 30d016d759
commit 70b70861f1
14 changed files with 369 additions and 160 deletions

View File

@ -20,5 +20,5 @@ interface CheatDao {
fun getEnabledRomCheats(gameCode: String, gameChecksum: String): Single<List<CheatEntity>>
@Update(entity = CheatEntity::class)
fun updateCheatsStatus(cheats: List<CheatStatusUpdate>)
suspend fun updateCheatsStatus(cheats: List<CheatStatusUpdate>)
}

View File

@ -4,15 +4,21 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import io.reactivex.Maybe
import kotlinx.coroutines.flow.Flow
import me.magnum.melonds.database.entities.CheatFolderWithCheats
import me.magnum.melonds.database.entities.GameEntity
import me.magnum.melonds.database.entities.GameWithCheatCategories
@Dao
interface GameDao {
@Transaction
@Query("SELECT * FROM game")
fun getGames(): Flow<List<GameEntity>>
@Query("SELECT * FROM game WHERE game_code = :gameCode AND (game_checksum IS NULL OR game_checksum = :gameChecksum)")
fun findGameWithCheats(gameCode: String, gameChecksum: String): Maybe<List<GameWithCheatCategories>>
suspend fun findGames(gameCode: String, gameChecksum: String): List<GameEntity>
@Transaction
@Query("SELECT * FROM cheat_folder WHERE game_id = :gameId")
suspend fun getGameCheats(gameId: Long): List<CheatFolderWithCheats>
@Insert
fun insertGame(game: GameEntity): Long

View File

@ -1,16 +1,22 @@
package me.magnum.melonds.domain.repositories
import android.net.Uri
import io.reactivex.Completable
import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.Single
import me.magnum.melonds.domain.model.*
import kotlinx.coroutines.flow.Flow
import me.magnum.melonds.domain.model.Cheat
import me.magnum.melonds.domain.model.CheatDatabase
import me.magnum.melonds.domain.model.CheatFolder
import me.magnum.melonds.domain.model.CheatImportProgress
import me.magnum.melonds.domain.model.Game
import me.magnum.melonds.domain.model.RomInfo
interface CheatsRepository {
fun getAllRomCheats(romInfo: RomInfo): Maybe<List<Game>>
suspend fun observeGames(): Flow<List<Game>>
suspend fun findGamesForRom(romInfo: RomInfo): List<Game>
suspend fun getAllGameCheats(game: Game): List<CheatFolder>
fun getRomEnabledCheats(romInfo: RomInfo): Single<List<Cheat>>
fun updateCheatsStatus(cheats: List<Cheat>): Completable
suspend fun updateCheatsStatus(cheats: List<Cheat>)
fun deleteCheatDatabaseIfExists(databaseName: String)
fun addCheatDatabase(databaseName: String): CheatDatabase
fun addGameCheats(databaseId: Long, game: Game)

View File

@ -3,16 +3,29 @@ package me.magnum.melonds.impl
import android.content.Context
import android.net.Uri
import androidx.lifecycle.Observer
import androidx.work.*
import io.reactivex.Completable
import io.reactivex.Maybe
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.workDataOf
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import me.magnum.melonds.common.workers.CheatImportWorker
import me.magnum.melonds.database.MelonDatabase
import me.magnum.melonds.database.entities.*
import me.magnum.melonds.domain.model.*
import me.magnum.melonds.database.entities.CheatDatabaseEntity
import me.magnum.melonds.database.entities.CheatEntity
import me.magnum.melonds.database.entities.CheatFolderEntity
import me.magnum.melonds.database.entities.CheatStatusUpdate
import me.magnum.melonds.database.entities.GameEntity
import me.magnum.melonds.domain.model.Cheat
import me.magnum.melonds.domain.model.CheatDatabase
import me.magnum.melonds.domain.model.CheatFolder
import me.magnum.melonds.domain.model.CheatImportProgress
import me.magnum.melonds.domain.model.Game
import me.magnum.melonds.domain.model.RomInfo
import me.magnum.melonds.domain.repositories.CheatsRepository
class RoomCheatsRepository(private val context: Context, private val database: MelonDatabase) : CheatsRepository {
@ -20,32 +33,50 @@ class RoomCheatsRepository(private val context: Context, private val database: M
private const val IMPORT_WORKER_NAME = "cheat_import_worker"
}
override fun getAllRomCheats(romInfo: RomInfo): Maybe<List<Game>> {
return database.gameDao().findGameWithCheats(romInfo.gameCode, romInfo.headerChecksumString()).map {
override suspend fun observeGames(): Flow<List<Game>> {
return database.gameDao().getGames().map {
it.map { game ->
Game(
game.game.id,
game.game.name,
game.game.gameCode,
game.game.gameChecksum,
game.cheatFolders.map { category ->
CheatFolder(
category.cheatFolder.id,
category.cheatFolder.name,
category.cheats.map { cheat ->
Cheat(
cheat.id,
cheat.name,
cheat.description,
cheat.code,
cheat.enabled
)
}
)
}
game.id,
game.name,
game.gameCode,
game.gameChecksum,
emptyList(),
)
}
}.subscribeOn(Schedulers.io())
}
}
override suspend fun findGamesForRom(romInfo: RomInfo): List<Game> {
return database.gameDao().findGames(romInfo.gameCode, romInfo.headerChecksumString()).map {
Game(
it.id,
it.name,
it.gameCode,
it.gameChecksum,
emptyList(),
)
}
}
override suspend fun getAllGameCheats(game: Game): List<CheatFolder> {
val gameId = game.id ?: return emptyList()
return database.gameDao().getGameCheats(gameId).map {
CheatFolder(
it.cheatFolder.id,
it.cheatFolder.name,
it.cheats.map { cheat ->
Cheat(
cheat.id,
cheat.name,
cheat.description,
cheat.code,
cheat.enabled
)
}
)
}
}
override fun getRomEnabledCheats(romInfo: RomInfo): Single<List<Cheat>> {
@ -62,15 +93,12 @@ class RoomCheatsRepository(private val context: Context, private val database: M
}.subscribeOn(Schedulers.io())
}
override fun updateCheatsStatus(cheats: List<Cheat>): Completable {
override suspend fun updateCheatsStatus(cheats: List<Cheat>) {
val cheatEntities = cheats.map {
CheatStatusUpdate(it.id!!, it.enabled)
}
return Completable.create {
database.cheatDao().updateCheatsStatus(cheatEntities)
it.onComplete()
}.subscribeOn(Schedulers.io())
database.cheatDao().updateCheatsStatus(cheatEntities)
}
override fun deleteCheatDatabaseIfExists(databaseName: String) {

View File

@ -7,9 +7,13 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.magnum.melonds.R
import me.magnum.melonds.databinding.ActivityCheatsBinding
@ -38,26 +42,33 @@ class CheatsActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
onBackPressedDispatcher.addCallback(backHandler)
viewModel.getRomCheats().observe(this) {
binding.progressBarCheats.isGone = true
if (savedInstanceState == null) {
openCheatsFragment()
}
if (it.isEmpty()) {
binding.textCheatsNotFound.isVisible = true
} else if (savedInstanceState == null) {
openCheatsFragment()
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.openEnabledCheatsEvent.collectLatest {
openEnabledCheatsFragment()
}
}
}
viewModel.openEnabledCheatsEvent.observe(this) {
openEnabledCheatsFragment()
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.committingCheatsChangesState.collectLatest {
binding.viewBlock.isGone = !it
}
}
}
viewModel.committingCheatsChangesStatus().observe(this) {
binding.viewBlock.isGone = !it
}
viewModel.onCheatChangesCommitted().observe(this) {
finish()
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.cheatChangesCommittedEvent.collectLatest {
if (!it) {
Toast.makeText(this@CheatsActivity, R.string.failed_save_cheat_changes, Toast.LENGTH_LONG).show()
}
finish()
}
}
}
}
@ -99,14 +110,10 @@ class CheatsActivity : AppCompatActivity() {
private fun commitCheatChangesAndFinish() {
// If changes are already being committed, do nothing
if (viewModel.committingCheatsChangesStatus().value == true) {
if (viewModel.committingCheatsChangesState.value) {
return
}
viewModel.commitCheatChanges().observe(this) {
if (!it) {
Toast.makeText(this, R.string.failed_save_cheat_changes, Toast.LENGTH_LONG).show()
}
}
viewModel.commitCheatChanges()
}
}

View File

@ -10,7 +10,12 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit
import androidx.fragment.app.replace
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.magnum.melonds.R
import me.magnum.melonds.databinding.FragmentCheatsBinding
import me.magnum.melonds.extensions.viewBinding
@ -50,21 +55,34 @@ class CheatsFragment : Fragment(R.layout.fragment_cheats) {
requireActivity().addMenuProvider(cheatsMenuProvider, viewLifecycleOwner)
if (isLaunchingForFirstTime) {
val hasMultipleGames = (viewModel.getRomCheats().value?.size ?: 0) > 1
if (hasMultipleGames) {
openSubScreenFragment<GamesSubScreenFragment>(isRootFragment = true)
} else {
openSubScreenFragment<FoldersSubScreenFragment>(isRootFragment = true)
if (isLaunchingForFirstTime || !viewModel.initialContentReady.value) {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.initialContentReady.collectLatest { ready ->
if (ready) {
val hasSelectedGame = viewModel.selectedGame.value != null
if (hasSelectedGame) {
openSubScreenFragment<FoldersSubScreenFragment>(isRootFragment = true)
} else {
openSubScreenFragment<GamesSubScreenFragment>(isRootFragment = true)
}
}
}
}
}
viewModel.openFoldersEvent.observe(viewLifecycleOwner) {
openSubScreenFragment<FoldersSubScreenFragment>()
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.openFoldersEvent.collectLatest {
openSubScreenFragment<FoldersSubScreenFragment>()
}
}
}
viewModel.openCheatsEvent.observe(viewLifecycleOwner) {
openSubScreenFragment<FolderCheatsScreenFragment>()
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.openCheatsEvent.collectLatest {
openSubScreenFragment<FolderCheatsScreenFragment>()
}
}
}
isLaunchingForFirstTime = false

View File

@ -1,19 +1,24 @@
package me.magnum.melonds.ui.cheats
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.disposables.CompositeDisposable
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import me.magnum.melonds.common.suspendRunCatching
import me.magnum.melonds.domain.model.Cheat
import me.magnum.melonds.domain.model.CheatFolder
import me.magnum.melonds.domain.model.CheatInFolder
import me.magnum.melonds.domain.model.Game
import me.magnum.melonds.domain.repositories.CheatsRepository
import me.magnum.melonds.extensions.addTo
import me.magnum.melonds.parcelables.RomInfoParcelable
import me.magnum.melonds.utils.SingleLiveEvent
import me.magnum.melonds.ui.cheats.model.CheatsScreenUiState
import javax.inject.Inject
@HiltViewModel
@ -22,63 +27,91 @@ class CheatsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val selectedGame = MutableLiveData<Game>()
private val selectedFolder = MutableLiveData<CheatFolder?>()
private var allRomCheatsLiveData = MutableLiveData<List<Game>>()
private val committingCheatsChangesStatusLiveData = MutableLiveData(false)
private val cheatChangesCommittedLiveEvent = SingleLiveEvent<Unit>()
private val modifiedCheatSet = mutableListOf<Cheat>()
private val _openFoldersEvent = SingleLiveEvent<Unit>()
val openFoldersEvent: LiveData<Unit> = _openFoldersEvent
private val _initialContentReady = MutableStateFlow(false)
val initialContentReady = _initialContentReady.asStateFlow()
private val _openCheatsEvent = SingleLiveEvent<Unit>()
val openCheatsEvent: LiveData<Unit> = _openCheatsEvent
private val _games = MutableStateFlow<CheatsScreenUiState<List<Game>>>(CheatsScreenUiState.Loading())
val games = _games.asStateFlow()
private val _openEnabledCheatsEvent = SingleLiveEvent<Unit>()
val openEnabledCheatsEvent: LiveData<Unit> = _openEnabledCheatsEvent
private val _selectedGame = MutableStateFlow<Game?>(null)
val selectedGame = _selectedGame.asStateFlow()
private val disposables = CompositeDisposable()
private val _selectedGameCheats = MutableStateFlow<CheatsScreenUiState<List<CheatFolder>>>(CheatsScreenUiState.Loading())
val selectedGameCheats: StateFlow<CheatsScreenUiState<List<CheatFolder>>> get() {
_selectedGameCheats.tryEmit(CheatsScreenUiState.Loading())
loadCheatsForSelectedGame()
return _selectedGameCheats.asStateFlow()
}
private val _selectedCheatFolder = MutableStateFlow<CheatFolder?>(null)
val selectedCheatFolder = _selectedCheatFolder.asStateFlow()
private val _openFoldersEvent = Channel<Unit>(Channel.CONFLATED)
val openFoldersEvent = _openFoldersEvent.receiveAsFlow()
private val _openCheatsEvent = Channel<Unit>(Channel.CONFLATED)
val openCheatsEvent = _openCheatsEvent.receiveAsFlow()
private val _openEnabledCheatsEvent = Channel<Unit>(Channel.CONFLATED)
val openEnabledCheatsEvent = _openEnabledCheatsEvent.receiveAsFlow()
private val _committingCheatsChangesState = MutableStateFlow(false)
val committingCheatsChangesState = _committingCheatsChangesState.asStateFlow()
private val _cheatChangesCommittedEvent = Channel<Boolean>(Channel.CONFLATED)
val cheatChangesCommittedEvent = _cheatChangesCommittedEvent.receiveAsFlow()
init {
val romInfo = savedStateHandle.get<RomInfoParcelable>(CheatsActivity.KEY_ROM_INFO) ?: error("No ROM info provided")
val romInfo = savedStateHandle.get<RomInfoParcelable>(CheatsActivity.KEY_ROM_INFO)
cheatsRepository.getAllRomCheats(romInfo.toRomInfo()).subscribe {
allRomCheatsLiveData.postValue(it)
if (it.size == 1) {
selectedGame.postValue(it.first())
viewModelScope.launch {
if (romInfo != null) {
val games = cheatsRepository.findGamesForRom(romInfo.toRomInfo())
_games.emit(CheatsScreenUiState.Ready(games))
if (games.size == 1) {
_selectedGame.emit(games.first())
}
_initialContentReady.emit(true)
} else {
cheatsRepository.observeGames().collectLatest {
_games.emit(CheatsScreenUiState.Ready(it))
_initialContentReady.emit(true)
}
/*cheatsRepository.getAllRomCheats(romInfo.toRomInfo()).subscribe {
allRomCheatsLiveData.postValue(it)
if (it.size == 1) {
_selectedGame.tryEmit(it.first())
}
_initialContentReady.tryEmit(true)
}.addTo(disposables)*/
}
}.addTo(disposables)
}
}
fun getRomCheats(): LiveData<List<Game>> {
return allRomCheatsLiveData
}
private fun loadCheatsForSelectedGame() {
val selectedGame = _selectedGame.value ?: return
fun getGames(): List<Game> {
return allRomCheatsLiveData.value ?: emptyList()
}
fun getSelectedGame(): LiveData<Game> {
return selectedGame
viewModelScope.launch {
val cheatFolders = cheatsRepository.getAllGameCheats(selectedGame)
_selectedGameCheats.emit(CheatsScreenUiState.Ready(cheatFolders))
}
}
fun setSelectedGame(game: Game) {
selectedGame.value = game
_openFoldersEvent.postValue(Unit)
_selectedGame.tryEmit(game)
_openFoldersEvent.trySend(Unit)
}
fun getSelectedFolder(): LiveData<CheatFolder?> {
return selectedFolder
}
fun setSelectedFolder(folder: CheatFolder?) {
selectedFolder.value = folder
_openCheatsEvent.postValue(Unit)
fun setSelectedFolder(folder: CheatFolder) {
_selectedCheatFolder.value = folder
_openCheatsEvent.trySend(Unit)
}
fun getSelectedFolderCheats(): List<Cheat> {
val cheats = selectedFolder.value?.cheats?.toMutableList() ?: mutableListOf()
val cheats = _selectedCheatFolder.value?.cheats?.toMutableList() ?: mutableListOf()
modifiedCheatSet.forEach { cheat ->
val originalCheatIndex = cheats.indexOfFirst { it.id == cheat.id }
@ -113,40 +146,24 @@ class CheatsViewModel @Inject constructor(
}
fun openEnabledCheats() {
_openEnabledCheatsEvent.postValue(Unit)
_openEnabledCheatsEvent.trySend(Unit)
}
fun committingCheatsChangesStatus(): LiveData<Boolean> {
return committingCheatsChangesStatusLiveData
}
fun onCheatChangesCommitted(): LiveData<Unit> {
return cheatChangesCommittedLiveEvent
}
fun commitCheatChanges(): LiveData<Boolean> {
val liveData = MutableLiveData<Boolean>()
fun commitCheatChanges() {
if (modifiedCheatSet.isEmpty()) {
cheatChangesCommittedLiveEvent.postValue(Unit)
return liveData
_cheatChangesCommittedEvent.trySend(true)
return
}
committingCheatsChangesStatusLiveData.value = true
cheatsRepository.updateCheatsStatus(modifiedCheatSet).doAfterTerminate {
committingCheatsChangesStatusLiveData.postValue(false)
cheatChangesCommittedLiveEvent.postValue(Unit)
}.subscribe({
liveData.postValue(true)
}, {
liveData.postValue(false)
}).addTo(disposables)
return liveData
}
override fun onCleared() {
super.onCleared()
disposables.dispose()
_committingCheatsChangesState.value = true
viewModelScope.launch {
suspendRunCatching {
cheatsRepository.updateCheatsStatus(modifiedCheatSet)
}.fold(
onSuccess = { _cheatChangesCommittedEvent.trySend(true) },
onFailure = { _cheatChangesCommittedEvent.trySend(false) },
)
_committingCheatsChangesState.value = false
}
}
}

View File

@ -11,7 +11,7 @@ import me.magnum.melonds.extensions.setViewEnabledRecursive
class FolderCheatsScreenFragment : SubScreenFragment() {
override fun getScreenName(): String? {
return viewModel.getSelectedFolder().value?.name
return viewModel.selectedCheatFolder.value?.name
}
override fun getSubScreenAdapter(): RecyclerView.Adapter<*> {

View File

@ -2,28 +2,46 @@ package me.magnum.melonds.ui.cheats
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.magnum.melonds.R
import me.magnum.melonds.databinding.ItemCheatsFolderBinding
import me.magnum.melonds.domain.model.CheatFolder
import me.magnum.melonds.ui.cheats.model.CheatsScreenUiState
import me.magnum.melonds.utils.SimpleDiffCallback
class FoldersSubScreenFragment : SubScreenFragment() {
override fun getSubScreenAdapter(): RecyclerView.Adapter<*> {
return FoldersAdapter(viewModel.getSelectedGame().value?.cheats ?: emptyList()) {
val adapter = FoldersAdapter {
viewModel.setSelectedFolder(it)
}
}
override fun getScreenName(): String? {
return if (viewModel.getGames().size == 1) {
getString(R.string.cheats)
} else {
viewModel.getSelectedGame().value?.name
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.selectedGameCheats.collectLatest {
updateScreenState(it)
when (it) {
is CheatsScreenUiState.Loading -> { }
is CheatsScreenUiState.Ready<*> -> adapter.updateCheatFolders(it.data as List<CheatFolder>)
}
}
}
}
return adapter
}
private class FoldersAdapter(val folders: List<CheatFolder>, private val onFolderClicked: (CheatFolder) -> Unit) : RecyclerView.Adapter<FoldersAdapter.ViewHolder>() {
override fun getScreenName(): String {
return viewModel.selectedGame.value?.name ?: getString(R.string.cheats)
}
private class FoldersAdapter(private val onFolderClicked: (CheatFolder) -> Unit) : RecyclerView.Adapter<FoldersAdapter.ViewHolder>() {
class ViewHolder(private val binding: ItemCheatsFolderBinding) : RecyclerView.ViewHolder(binding.root) {
private lateinit var folder: CheatFolder
@ -38,6 +56,17 @@ class FoldersSubScreenFragment : SubScreenFragment() {
}
}
private val folders = mutableListOf<CheatFolder>()
fun updateCheatFolders(newCheatFolders: List<CheatFolder>) {
val diffResult = DiffUtil.calculateDiff(FoldersDiffCallback(folders, newCheatFolders))
diffResult.dispatchUpdatesTo(this)
folders.apply {
clear()
addAll(newCheatFolders)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemCheatsFolderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding).apply {
@ -54,5 +83,11 @@ class FoldersSubScreenFragment : SubScreenFragment() {
override fun getItemCount(): Int {
return folders.size
}
class FoldersDiffCallback(oldList: List<CheatFolder>, newList: List<CheatFolder>): SimpleDiffCallback<CheatFolder>(oldList, newList) {
override fun areItemsTheSame(old: CheatFolder, new: CheatFolder): Boolean {
return old.id == new.id
}
}
}
}

View File

@ -2,24 +2,45 @@ package me.magnum.melonds.ui.cheats
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import me.magnum.melonds.R
import me.magnum.melonds.databinding.ItemCheatsGameBinding
import me.magnum.melonds.domain.model.Game
import me.magnum.melonds.ui.cheats.model.CheatsScreenUiState
class GamesSubScreenFragment : SubScreenFragment() {
override fun getSubScreenAdapter(): RecyclerView.Adapter<*> {
return GamesAdapter(viewModel.getGames()) {
val adapter = GamesAdapter {
viewModel.setSelectedGame(it)
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.games.collectLatest {
updateScreenState(it)
when (it) {
is CheatsScreenUiState.Loading -> { }
is CheatsScreenUiState.Ready<*> -> adapter.updateGames(it.data as List<Game>)
}
}
}
}
return adapter
}
override fun getScreenName(): String {
return getString(R.string.cheats)
}
private class GamesAdapter(val games: List<Game>, private val onGameClicked: (Game) -> Unit) : RecyclerView.Adapter<GamesAdapter.ViewHolder>() {
private class GamesAdapter(private val onGameClicked: (Game) -> Unit) : RecyclerView.Adapter<GamesAdapter.ViewHolder>() {
class ViewHolder(private val binding: ItemCheatsGameBinding) : RecyclerView.ViewHolder(binding.root) {
private lateinit var game: Game
@ -34,6 +55,8 @@ class GamesSubScreenFragment : SubScreenFragment() {
}
}
private var games = emptyList<Game>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemCheatsGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding).apply {
@ -43,6 +66,12 @@ class GamesSubScreenFragment : SubScreenFragment() {
}
}
fun updateGames(newGames: List<Game>) {
val result = DiffUtil.calculateDiff(GamesDillCallback(games, newGames))
result.dispatchUpdatesTo(this)
games = newGames
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.setGame(games[position])
}
@ -50,5 +79,19 @@ class GamesSubScreenFragment : SubScreenFragment() {
override fun getItemCount(): Int {
return games.size
}
private class GamesDillCallback(val oldGames: List<Game>, val newGames: List<Game>) : DiffUtil.Callback() {
override fun getOldListSize() = oldGames.size
override fun getNewListSize() = newGames.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldGames[oldItemPosition].id == newGames[newItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldGames[oldItemPosition] == newGames[newItemPosition]
}
}
}
}

View File

@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -12,6 +13,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import me.magnum.melonds.databinding.FragmentCheatsSubscreenBinding
import me.magnum.melonds.ui.cheats.model.CheatsScreenUiState
abstract class SubScreenFragment : Fragment() {
protected val viewModel: CheatsViewModel by activityViewModels()
@ -31,7 +33,6 @@ abstract class SubScreenFragment : Fragment() {
addItemDecoration(DividerItemDecoration(context, listLayoutManager.orientation))
adapter = getSubScreenAdapter()
}
binding.listItems.adapter?.notifyDataSetChanged()
if (binding.listItems.adapter?.itemCount == 0) {
getNoContentText()?.let {
@ -45,6 +46,21 @@ abstract class SubScreenFragment : Fragment() {
(requireActivity() as AppCompatActivity).supportActionBar?.title = getScreenName()
}
fun updateScreenState(uiState: CheatsScreenUiState<*>) {
when (uiState) {
is CheatsScreenUiState.Loading -> {
binding.progressBar.isVisible = true
binding.textNoContent.isGone = true
binding.listItems.isVisible = false
}
is CheatsScreenUiState.Ready -> {
binding.progressBar.isGone = true
binding.textNoContent.isGone = true
binding.listItems.isVisible = true
}
}
}
abstract fun getSubScreenAdapter(): RecyclerView.Adapter<*>
abstract fun getScreenName(): String?

View File

@ -0,0 +1,6 @@
package me.magnum.melonds.ui.cheats.model
sealed class CheatsScreenUiState<T> {
class Loading<T> : CheatsScreenUiState<T>()
data class Ready<T>(val data: T) : CheatsScreenUiState<T>()
}

View File

@ -0,0 +1,19 @@
package me.magnum.melonds.utils
import androidx.recyclerview.widget.DiffUtil
abstract class SimpleDiffCallback<T>(private val oldList: List<T>, private val newList: List<T>) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return areItemsTheSame(oldList[oldItemPosition], newList[newItemPosition])
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
abstract fun areItemsTheSame(old: T, new: T): Boolean
}

View File

@ -19,4 +19,12 @@
android:layout_centerVertical="true"
android:layout_margin="24dp"
android:visibility="gone" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:visibility="gone" />
</RelativeLayout>