From 4a3cbf00216918a53b143f6bbfc4b22a49761770 Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Thu, 31 Aug 2023 13:32:48 -0400 Subject: [PATCH] android: Use StateFlow instead of LiveData --- .../yuzu_emu/adapters/HomeSettingAdapter.kt | 10 +- .../features/settings/ui/SettingsActivity.kt | 53 ++++++--- .../features/settings/ui/SettingsFragment.kt | 43 ++++--- .../yuzu_emu/fragments/EmulationFragment.kt | 110 +++++++++++------- .../IndeterminateProgressDialogFragment.kt | 32 +++-- .../yuzu/yuzu_emu/fragments/SearchFragment.kt | 46 +++++--- .../fragments/SettingsSearchFragment.kt | 16 ++- .../yuzu/yuzu_emu/fragments/SetupFragment.kt | 16 ++- .../yuzu/yuzu_emu/model/EmulationViewModel.kt | 44 ++++--- .../org/yuzu/yuzu_emu/model/GamesViewModel.kt | 44 +++---- .../org/yuzu/yuzu_emu/model/HomeSetting.kt | 6 +- .../org/yuzu/yuzu_emu/model/HomeViewModel.kt | 28 ++--- .../yuzu/yuzu_emu/model/SettingsViewModel.kt | 57 ++++----- .../org/yuzu/yuzu_emu/model/TaskViewModel.kt | 27 ++--- .../org/yuzu/yuzu_emu/ui/GamesFragment.kt | 66 +++++++---- .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 20 +++- 16 files changed, 368 insertions(+), 250 deletions(-) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt index 8d87d3bd7..1675627a1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt @@ -10,8 +10,12 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat +import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding import org.yuzu.yuzu_emu.fragments.MessageDialogFragment @@ -86,7 +90,11 @@ class HomeSettingAdapter( binding.optionIcon.alpha = 0.5f } - option.details.observe(viewLifecycle) { updateOptionDetails(it) } + viewLifecycle.lifecycleScope.launch { + viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { + option.details.collect { updateOptionDetails(it) } + } + } binding.optionDetail.postDelayed( { binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 908c01265..4d2f2f604 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt @@ -13,9 +13,14 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.NavHostFragment import androidx.navigation.navArgs import com.google.android.material.color.MaterialColors +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import java.io.IOException import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding @@ -66,25 +71,39 @@ class SettingsActivity : AppCompatActivity() { ) } - settingsViewModel.shouldRecreate.observe(this) { - if (it) { - settingsViewModel.setShouldRecreate(false) - recreate() + lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + settingsViewModel.shouldRecreate.collectLatest { + if (it) { + settingsViewModel.setShouldRecreate(false) + recreate() + } + } + } } - } - settingsViewModel.shouldNavigateBack.observe(this) { - if (it) { - settingsViewModel.setShouldNavigateBack(false) - navigateBack() + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + settingsViewModel.shouldNavigateBack.collectLatest { + if (it) { + settingsViewModel.setShouldNavigateBack(false) + navigateBack() + } + } + } } - } - settingsViewModel.shouldShowResetSettingsDialog.observe(this) { - if (it) { - settingsViewModel.setShouldShowResetSettingsDialog(false) - ResetSettingsDialogFragment().show( - supportFragmentManager, - ResetSettingsDialogFragment.TAG - ) + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + settingsViewModel.shouldShowResetSettingsDialog.collectLatest { + if (it) { + settingsViewModel.setShouldShowResetSettingsDialog(false) + ResetSettingsDialogFragment().show( + supportFragmentManager, + ResetSettingsDialogFragment.TAG + ) + } + } + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index bc319714c..2a816183a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt @@ -13,11 +13,15 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile @@ -51,6 +55,8 @@ class SettingsFragment : Fragment() { return binding.root } + // This is using the correct scope, lint is just acting up + @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { settingsAdapter = SettingsAdapter(this, requireContext()) presenter = SettingsFragmentPresenter( @@ -75,24 +81,27 @@ class SettingsFragment : Fragment() { settingsViewModel.setShouldNavigateBack(true) } - settingsViewModel.toolbarTitle.observe(viewLifecycleOwner) { - if (it.isNotEmpty()) binding.toolbarSettingsLayout.title = it - } - - settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) { - if (it) { - settingsViewModel.setShouldReloadSettingsList(false) - presenter.loadSettingsList() + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + settingsViewModel.shouldReloadSettingsList.collectLatest { + if (it) { + settingsViewModel.setShouldReloadSettingsList(false) + presenter.loadSettingsList() + } + } + } } - } - - settingsViewModel.isUsingSearch.observe(viewLifecycleOwner) { - if (it) { - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - } else { - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + launch { + settingsViewModel.isUsingSearch.collectLatest { + if (it) { + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + } else { + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + } + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 944ae652e..1addb2326 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -39,6 +39,7 @@ import androidx.window.layout.WindowLayoutInfo import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.slider.Slider import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary @@ -129,6 +130,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { return binding.root } + // This is using the correct scope, lint is just acting up + @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding.surfaceEmulation.holder.addCallback(this) binding.showFpsText.setTextColor(Color.YELLOW) @@ -205,59 +208,80 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } ) - viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { - lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - WindowInfoTracker.getOrCreate(requireContext()) - .windowLayoutInfo(requireActivity()) - .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) } - } - } - GameIconUtils.loadGameIcon(game, binding.loadingImage) binding.loadingTitle.text = game.title binding.loadingTitle.isSelected = true binding.loadingText.isSelected = true - emulationViewModel.shaderProgress.observe(viewLifecycleOwner) { - if (it > 0 && it != emulationViewModel.totalShaders.value!!) { - binding.loadingProgressIndicator.isIndeterminate = false - - if (it < binding.loadingProgressIndicator.max) { - binding.loadingProgressIndicator.progress = it + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + WindowInfoTracker.getOrCreate(requireContext()) + .windowLayoutInfo(requireActivity()) + .collect { + updateFoldableLayout(requireActivity() as EmulationActivity, it) + } } } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + emulationViewModel.shaderProgress.collectLatest { + if (it > 0 && it != emulationViewModel.totalShaders.value) { + binding.loadingProgressIndicator.isIndeterminate = false - if (it == emulationViewModel.totalShaders.value!!) { - binding.loadingText.setText(R.string.loading) - binding.loadingProgressIndicator.isIndeterminate = true + if (it < binding.loadingProgressIndicator.max) { + binding.loadingProgressIndicator.progress = it + } + } + + if (it == emulationViewModel.totalShaders.value) { + binding.loadingText.setText(R.string.loading) + binding.loadingProgressIndicator.isIndeterminate = true + } + } + } } - } - emulationViewModel.totalShaders.observe(viewLifecycleOwner) { - binding.loadingProgressIndicator.max = it - } - emulationViewModel.shaderMessage.observe(viewLifecycleOwner) { - if (it.isNotEmpty()) { - binding.loadingText.text = it + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + emulationViewModel.totalShaders.collectLatest { + binding.loadingProgressIndicator.max = it + } + } } - } - - emulationViewModel.emulationStarted.observe(viewLifecycleOwner) { started -> - if (started) { - binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) - ViewUtils.showView(binding.surfaceInputOverlay) - ViewUtils.hideView(binding.loadingIndicator) - - // Setup overlay - updateShowFpsOverlay() + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + emulationViewModel.shaderMessage.collectLatest { + if (it.isNotEmpty()) { + binding.loadingText.text = it + } + } + } } - } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + emulationViewModel.emulationStarted.collectLatest { + if (it) { + binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + ViewUtils.showView(binding.surfaceInputOverlay) + ViewUtils.hideView(binding.loadingIndicator) - emulationViewModel.isEmulationStopping.observe(viewLifecycleOwner) { - if (it) { - binding.loadingText.setText(R.string.shutting_down) - ViewUtils.showView(binding.loadingIndicator) - ViewUtils.hideView(binding.inputContainer) - ViewUtils.hideView(binding.showFpsText) + // Setup overlay + updateShowFpsOverlay() + } + } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + emulationViewModel.isEmulationStopping.collectLatest { + if (it) { + binding.loadingText.setText(R.string.shutting_down) + ViewUtils.showView(binding.loadingIndicator) + ViewUtils.hideView(binding.inputContainer) + ViewUtils.hideView(binding.showFpsText) + } + } + } } } } @@ -274,9 +298,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } } else { - if (EmulationMenuSettings.showOverlay && - emulationViewModel.emulationStarted.value == true - ) { + if (EmulationMenuSettings.showOverlay && emulationViewModel.emulationStarted.value) { binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.visibility = View.VISIBLE } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt index 181bd983a..ea8eb073a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt @@ -9,8 +9,12 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.model.TaskViewModel @@ -28,21 +32,27 @@ class IndeterminateProgressDialogFragment : DialogFragment() { .create() dialog.setCanceledOnTouchOutside(false) - taskViewModel.isComplete.observe(this) { complete -> - if (complete) { - dialog.dismiss() - when (val result = taskViewModel.result.value) { - is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show() - is MessageDialogFragment -> result.show( - requireActivity().supportFragmentManager, - MessageDialogFragment.TAG - ) + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + taskViewModel.isComplete.collect { + if (it) { + dialog.dismiss() + when (val result = taskViewModel.result.value) { + is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG) + .show() + + is MessageDialogFragment -> result.show( + requireActivity().supportFragmentManager, + MessageDialogFragment.TAG + ) + } + taskViewModel.clear() + } } - taskViewModel.clear() } } - if (taskViewModel.isRunning.value == false) { + if (!taskViewModel.isRunning.value) { taskViewModel.runTask() } return dialog diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt index f54dccc69..2dbca76a5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt @@ -3,6 +3,7 @@ package org.yuzu.yuzu_emu.fragments +import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.os.Bundle @@ -17,9 +18,13 @@ import androidx.core.view.updatePadding import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.preference.PreferenceManager import info.debatty.java.stringsimilarity.Jaccard import info.debatty.java.stringsimilarity.JaroWinkler +import kotlinx.coroutines.launch import java.util.Locale import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication @@ -52,6 +57,8 @@ class SearchFragment : Fragment() { return binding.root } + // This is using the correct scope, lint is just acting up + @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { homeViewModel.setNavigationVisibility(visible = true, animated = false) preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) @@ -79,21 +86,32 @@ class SearchFragment : Fragment() { filterAndSearch() } - gamesViewModel.apply { - searchFocused.observe(viewLifecycleOwner) { searchFocused -> - if (searchFocused) { - focusSearch() - gamesViewModel.setSearchFocused(false) + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + gamesViewModel.searchFocused.collect { + if (it) { + focusSearch() + gamesViewModel.setSearchFocused(false) + } + } } } - - games.observe(viewLifecycleOwner) { filterAndSearch() } - searchedGames.observe(viewLifecycleOwner) { - (binding.gridGamesSearch.adapter as GameAdapter).submitList(it) - if (it.isEmpty()) { - binding.noResultsView.visibility = View.VISIBLE - } else { - binding.noResultsView.visibility = View.GONE + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + gamesViewModel.games.collect { filterAndSearch() } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + gamesViewModel.searchedGames.collect { + (binding.gridGamesSearch.adapter as GameAdapter).submitList(it) + if (it.isEmpty()) { + binding.noResultsView.visibility = View.VISIBLE + } else { + binding.noResultsView.visibility = View.GONE + } + } } } } @@ -109,7 +127,7 @@ class SearchFragment : Fragment() { private inner class ScoredGame(val score: Double, val item: Game) private fun filterAndSearch() { - val baseList = gamesViewModel.games.value!! + val baseList = gamesViewModel.games.value val filteredList: List = when (binding.chipGroup.checkedChipId) { R.id.chip_recently_played -> { baseList.filter { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt index 55b6a0367..9d0594c6e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt @@ -15,10 +15,14 @@ import androidx.core.view.updatePadding import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.transition.MaterialSharedAxis import info.debatty.java.stringsimilarity.Cosine +import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem @@ -79,10 +83,14 @@ class SettingsSearchFragment : Fragment() { search() binding.settingsList.smoothScrollToPosition(0) } - settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) { - if (it) { - settingsViewModel.setShouldReloadSettingsList(false) - search() + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + settingsViewModel.shouldReloadSettingsList.collect { + if (it) { + settingsViewModel.setShouldReloadSettingsList(false) + search() + } + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index d50c421a0..fbb2f6e18 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt @@ -22,10 +22,14 @@ import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController import androidx.preference.PreferenceManager import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.google.android.material.transition.MaterialFadeThrough +import kotlinx.coroutines.launch import java.io.File import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication @@ -206,10 +210,14 @@ class SetupFragment : Fragment() { ) } - homeViewModel.shouldPageForward.observe(viewLifecycleOwner) { - if (it) { - pageForward() - homeViewModel.setShouldPageForward(false) + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + homeViewModel.shouldPageForward.collect { + if (it) { + pageForward() + homeViewModel.setShouldPageForward(false) + } + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt index e35f51bc3..f34870c2d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt @@ -3,28 +3,28 @@ package org.yuzu.yuzu_emu.model -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class EmulationViewModel : ViewModel() { - private val _emulationStarted = MutableLiveData(false) - val emulationStarted: LiveData get() = _emulationStarted + val emulationStarted: StateFlow get() = _emulationStarted + private val _emulationStarted = MutableStateFlow(false) - private val _isEmulationStopping = MutableLiveData(false) - val isEmulationStopping: LiveData get() = _isEmulationStopping + val isEmulationStopping: StateFlow get() = _isEmulationStopping + private val _isEmulationStopping = MutableStateFlow(false) - private val _shaderProgress = MutableLiveData(0) - val shaderProgress: LiveData get() = _shaderProgress + val shaderProgress: StateFlow get() = _shaderProgress + private val _shaderProgress = MutableStateFlow(0) - private val _totalShaders = MutableLiveData(0) - val totalShaders: LiveData get() = _totalShaders + val totalShaders: StateFlow get() = _totalShaders + private val _totalShaders = MutableStateFlow(0) - private val _shaderMessage = MutableLiveData("") - val shaderMessage: LiveData get() = _shaderMessage + val shaderMessage: StateFlow get() = _shaderMessage + private val _shaderMessage = MutableStateFlow("") fun setEmulationStarted(started: Boolean) { - _emulationStarted.postValue(started) + _emulationStarted.value = started } fun setIsEmulationStopping(value: Boolean) { @@ -50,10 +50,18 @@ class EmulationViewModel : ViewModel() { } fun clear() { - _emulationStarted.value = false - _isEmulationStopping.value = false - _shaderProgress.value = 0 - _totalShaders.value = 0 - _shaderMessage.value = "" + setEmulationStarted(false) + setIsEmulationStopping(false) + setShaderProgress(0) + setTotalShaders(0) + setShaderMessage("") + } + + companion object { + const val KEY_EMULATION_STARTED = "EmulationStarted" + const val KEY_IS_EMULATION_STOPPING = "IsEmulationStarting" + const val KEY_SHADER_PROGRESS = "ShaderProgress" + const val KEY_TOTAL_SHADERS = "TotalShaders" + const val KEY_SHADER_MESSAGE = "ShaderMessage" } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt index 1fe42f922..6e09fa81d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt @@ -5,13 +5,13 @@ package org.yuzu.yuzu_emu.model import android.net.Uri import androidx.documentfile.provider.DocumentFile -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import java.util.Locale import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi @@ -24,23 +24,23 @@ import org.yuzu.yuzu_emu.utils.GameHelper @OptIn(ExperimentalSerializationApi::class) class GamesViewModel : ViewModel() { - private val _games = MutableLiveData>(emptyList()) - val games: LiveData> get() = _games + val games: StateFlow> get() = _games + private val _games = MutableStateFlow(emptyList()) - private val _searchedGames = MutableLiveData>(emptyList()) - val searchedGames: LiveData> get() = _searchedGames + val searchedGames: StateFlow> get() = _searchedGames + private val _searchedGames = MutableStateFlow(emptyList()) - private val _isReloading = MutableLiveData(false) - val isReloading: LiveData get() = _isReloading + val isReloading: StateFlow get() = _isReloading + private val _isReloading = MutableStateFlow(false) - private val _shouldSwapData = MutableLiveData(false) - val shouldSwapData: LiveData get() = _shouldSwapData + val shouldSwapData: StateFlow get() = _shouldSwapData + private val _shouldSwapData = MutableStateFlow(false) - private val _shouldScrollToTop = MutableLiveData(false) - val shouldScrollToTop: LiveData get() = _shouldScrollToTop + val shouldScrollToTop: StateFlow get() = _shouldScrollToTop + private val _shouldScrollToTop = MutableStateFlow(false) - private val _searchFocused = MutableLiveData(false) - val searchFocused: LiveData get() = _searchFocused + val searchFocused: StateFlow get() = _searchFocused + private val _searchFocused = MutableStateFlow(false) init { // Ensure keys are loaded so that ROM metadata can be decrypted. @@ -79,36 +79,36 @@ class GamesViewModel : ViewModel() { ) ) - _games.postValue(sortedList) + _games.value = sortedList } fun setSearchedGames(games: List) { - _searchedGames.postValue(games) + _searchedGames.value = games } fun setShouldSwapData(shouldSwap: Boolean) { - _shouldSwapData.postValue(shouldSwap) + _shouldSwapData.value = shouldSwap } fun setShouldScrollToTop(shouldScroll: Boolean) { - _shouldScrollToTop.postValue(shouldScroll) + _shouldScrollToTop.value = shouldScroll } fun setSearchFocused(searchFocused: Boolean) { - _searchFocused.postValue(searchFocused) + _searchFocused.value = searchFocused } fun reloadGames(directoryChanged: Boolean) { - if (isReloading.value == true) { + if (isReloading.value) { return } - _isReloading.postValue(true) + _isReloading.value = true viewModelScope.launch { withContext(Dispatchers.IO) { NativeLibrary.resetRomMetadata() setGames(GameHelper.getGames()) - _isReloading.postValue(false) + _isReloading.value = false if (directoryChanged) { setShouldSwapData(true) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt index 498c222fa..b32e19373 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt @@ -3,8 +3,8 @@ package org.yuzu.yuzu_emu.model -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow data class HomeSetting( val titleId: Int, @@ -14,5 +14,5 @@ data class HomeSetting( val isEnabled: () -> Boolean = { true }, val disabledTitleId: Int = 0, val disabledMessageId: Int = 0, - val details: LiveData = MutableLiveData("") + val details: StateFlow = MutableStateFlow("") ) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt index a48ef7a88..756f76721 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt @@ -5,47 +5,43 @@ package org.yuzu.yuzu_emu.model import android.net.Uri import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.utils.GameHelper class HomeViewModel : ViewModel() { - private val _navigationVisible = MutableLiveData>() - val navigationVisible: LiveData> get() = _navigationVisible + val navigationVisible: StateFlow> get() = _navigationVisible + private val _navigationVisible = MutableStateFlow(Pair(false, false)) - private val _statusBarShadeVisible = MutableLiveData(true) - val statusBarShadeVisible: LiveData get() = _statusBarShadeVisible + val statusBarShadeVisible: StateFlow get() = _statusBarShadeVisible + private val _statusBarShadeVisible = MutableStateFlow(true) - private val _shouldPageForward = MutableLiveData(false) - val shouldPageForward: LiveData get() = _shouldPageForward + val shouldPageForward: StateFlow get() = _shouldPageForward + private val _shouldPageForward = MutableStateFlow(false) - private val _gamesDir = MutableLiveData( + val gamesDir: StateFlow get() = _gamesDir + private val _gamesDir = MutableStateFlow( Uri.parse( PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) .getString(GameHelper.KEY_GAME_PATH, "") ).path ?: "" ) - val gamesDir: LiveData get() = _gamesDir var navigatedToSetup = false - init { - _navigationVisible.value = Pair(false, false) - } - fun setNavigationVisibility(visible: Boolean, animated: Boolean) { - if (_navigationVisible.value?.first == visible) { + if (navigationVisible.value.first == visible) { return } _navigationVisible.value = Pair(visible, animated) } fun setStatusBarShadeVisibility(visible: Boolean) { - if (_statusBarShadeVisible.value == visible) { + if (statusBarShadeVisible.value == visible) { return } _statusBarShadeVisible.value = visible diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt index d16d15fa6..53fa7a8de 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt @@ -3,48 +3,43 @@ package org.yuzu.yuzu_emu.model -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem -class SettingsViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { +class SettingsViewModel : ViewModel() { var game: Game? = null var shouldSave = false var clickedItem: SettingsItem? = null - private val _toolbarTitle = MutableLiveData("") - val toolbarTitle: LiveData get() = _toolbarTitle + val shouldRecreate: StateFlow get() = _shouldRecreate + private val _shouldRecreate = MutableStateFlow(false) - private val _shouldRecreate = MutableLiveData(false) - val shouldRecreate: LiveData get() = _shouldRecreate + val shouldNavigateBack: StateFlow get() = _shouldNavigateBack + private val _shouldNavigateBack = MutableStateFlow(false) - private val _shouldNavigateBack = MutableLiveData(false) - val shouldNavigateBack: LiveData get() = _shouldNavigateBack + val shouldShowResetSettingsDialog: StateFlow get() = _shouldShowResetSettingsDialog + private val _shouldShowResetSettingsDialog = MutableStateFlow(false) - private val _shouldShowResetSettingsDialog = MutableLiveData(false) - val shouldShowResetSettingsDialog: LiveData get() = _shouldShowResetSettingsDialog + val shouldReloadSettingsList: StateFlow get() = _shouldReloadSettingsList + private val _shouldReloadSettingsList = MutableStateFlow(false) - private val _shouldReloadSettingsList = MutableLiveData(false) - val shouldReloadSettingsList: LiveData get() = _shouldReloadSettingsList + val isUsingSearch: StateFlow get() = _isUsingSearch + private val _isUsingSearch = MutableStateFlow(false) - private val _isUsingSearch = MutableLiveData(false) - val isUsingSearch: LiveData get() = _isUsingSearch + val sliderProgress: StateFlow get() = _sliderProgress + private val _sliderProgress = MutableStateFlow(-1) - val sliderProgress = savedStateHandle.getStateFlow(KEY_SLIDER_PROGRESS, -1) + val sliderTextValue: StateFlow get() = _sliderTextValue + private val _sliderTextValue = MutableStateFlow("") - val sliderTextValue = savedStateHandle.getStateFlow(KEY_SLIDER_TEXT_VALUE, "") - - val adapterItemChanged = savedStateHandle.getStateFlow(KEY_ADAPTER_ITEM_CHANGED, -1) - - fun setToolbarTitle(value: String) { - _toolbarTitle.value = value - } + val adapterItemChanged: StateFlow get() = _adapterItemChanged + private val _adapterItemChanged = MutableStateFlow(-1) fun setShouldRecreate(value: Boolean) { _shouldRecreate.value = value @@ -67,8 +62,8 @@ class SettingsViewModel(private val savedStateHandle: SavedStateHandle) : ViewMo } fun setSliderTextValue(value: Float, units: String) { - savedStateHandle[KEY_SLIDER_PROGRESS] = value - savedStateHandle[KEY_SLIDER_TEXT_VALUE] = String.format( + _sliderProgress.value = value.toInt() + _sliderTextValue.value = String.format( YuzuApplication.appContext.getString(R.string.value_with_units), value.toInt().toString(), units @@ -76,21 +71,15 @@ class SettingsViewModel(private val savedStateHandle: SavedStateHandle) : ViewMo } fun setSliderProgress(value: Float) { - savedStateHandle[KEY_SLIDER_PROGRESS] = value + _sliderProgress.value = value.toInt() } fun setAdapterItemChanged(value: Int) { - savedStateHandle[KEY_ADAPTER_ITEM_CHANGED] = value + _adapterItemChanged.value = value } fun clear() { game = null shouldSave = false } - - companion object { - const val KEY_SLIDER_TEXT_VALUE = "SliderTextValue" - const val KEY_SLIDER_PROGRESS = "SliderProgress" - const val KEY_ADAPTER_ITEM_CHANGED = "AdapterItemChanged" - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt index 27ea725a5..531c2aaf0 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt @@ -3,29 +3,25 @@ package org.yuzu.yuzu_emu.model -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch class TaskViewModel : ViewModel() { - private val _result = MutableLiveData() - val result: LiveData = _result + val result: StateFlow get() = _result + private val _result = MutableStateFlow(Any()) - private val _isComplete = MutableLiveData() - val isComplete: LiveData = _isComplete + val isComplete: StateFlow get() = _isComplete + private val _isComplete = MutableStateFlow(false) - private val _isRunning = MutableLiveData() - val isRunning: LiveData = _isRunning + val isRunning: StateFlow get() = _isRunning + private val _isRunning = MutableStateFlow(false) lateinit var task: () -> Any - init { - clear() - } - fun clear() { _result.value = Any() _isComplete.value = false @@ -33,15 +29,16 @@ class TaskViewModel : ViewModel() { } fun runTask() { - if (_isRunning.value == true) { + if (isRunning.value) { return } _isRunning.value = true viewModelScope.launch(Dispatchers.IO) { val res = task() - _result.postValue(res) - _isComplete.postValue(true) + _result.value = res + _isComplete.value = true + _isRunning.value = false } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt index b0156dca5..35e365458 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt @@ -3,6 +3,7 @@ package org.yuzu.yuzu_emu.ui +import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -14,8 +15,12 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.color.MaterialColors import com.google.android.material.transition.MaterialFadeThrough +import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.adapters.GameAdapter import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding @@ -44,6 +49,8 @@ class GamesFragment : Fragment() { return binding.root } + // This is using the correct scope, lint is just acting up + @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { homeViewModel.setNavigationVisibility(visible = true, animated = false) @@ -80,37 +87,48 @@ class GamesFragment : Fragment() { if (_binding == null) { return@post } - binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value!! + binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value } } - gamesViewModel.apply { - // Watch for when we get updates to any of our games lists - isReloading.observe(viewLifecycleOwner) { isReloading -> - binding.swipeRefresh.isRefreshing = isReloading - } - games.observe(viewLifecycleOwner) { - (binding.gridGames.adapter as GameAdapter).submitList(it) - if (it.isEmpty()) { - binding.noticeText.visibility = View.VISIBLE - } else { - binding.noticeText.visibility = View.GONE + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + gamesViewModel.isReloading.collect { binding.swipeRefresh.isRefreshing = it } } } - shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData -> - if (shouldSwapData) { - (binding.gridGames.adapter as GameAdapter).submitList( - gamesViewModel.games.value!! - ) - gamesViewModel.setShouldSwapData(false) + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + gamesViewModel.games.collect { + (binding.gridGames.adapter as GameAdapter).submitList(it) + if (it.isEmpty()) { + binding.noticeText.visibility = View.VISIBLE + } else { + binding.noticeText.visibility = View.GONE + } + } } } - - // Check if the user reselected the games menu item and then scroll to top of the list - shouldScrollToTop.observe(viewLifecycleOwner) { shouldScroll -> - if (shouldScroll) { - scrollToTop() - gamesViewModel.setShouldScrollToTop(false) + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + gamesViewModel.shouldSwapData.collect { + if (it) { + (binding.gridGames.adapter as GameAdapter).submitList( + gamesViewModel.games.value + ) + gamesViewModel.setShouldSwapData(false) + } + } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + gamesViewModel.shouldScrollToTop.collect { + if (it) { + scrollToTop() + gamesViewModel.setShouldScrollToTop(false) + } + } } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 7d8e06ad8..1ee833cf6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -19,7 +19,9 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController @@ -115,16 +117,22 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } // Prevents navigation from being drawn for a short time on recreation if set to hidden - if (!homeViewModel.navigationVisible.value?.first!!) { + if (!homeViewModel.navigationVisible.value.first) { binding.navigationView.visibility = View.INVISIBLE binding.statusBarShade.visibility = View.INVISIBLE } - homeViewModel.navigationVisible.observe(this) { - showNavigation(it.first, it.second) - } - homeViewModel.statusBarShadeVisible.observe(this) { visible -> - showStatusBarShade(visible) + lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + homeViewModel.navigationVisible.collect { showNavigation(it.first, it.second) } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } + } + } } // Dismiss previous notifications (should not happen unless a crash occurred)