From aa55cdca76307272321203119ef27d6a977a10dd Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:12:45 +0800 Subject: [PATCH] Refactor CheckUpdateActivity to MVVM architecture --- .../com/v2ray/ang/ui/CheckUpdateActivity.kt | 79 +++++-------------- .../com/v2ray/ang/viewmodel/BaseViewModel.kt | 15 ++++ .../ang/viewmodel/CheckUpdateViewModel.kt | 56 +++++++++++++ 3 files changed, 92 insertions(+), 58 deletions(-) create mode 100644 V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/CheckUpdateViewModel.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt index 56dcca95..a97becba 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt @@ -1,6 +1,7 @@ package com.v2ray.ang.ui import android.os.Bundle +import androidx.activity.viewModels import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -15,88 +16,53 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import com.v2ray.ang.AppConfig +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.v2ray.ang.BuildConfig import com.v2ray.ang.R import com.v2ray.ang.compose.AppTopBar -import com.v2ray.ang.compose.LocalAppSnackbar import com.v2ray.ang.compose.SettingsMenuItem import com.v2ray.ang.compose.SettingsSwitchItem -import com.v2ray.ang.compose.ToastType import com.v2ray.ang.compose.VersionInfoBlock import com.v2ray.ang.core.CoreNativeManager -import com.v2ray.ang.dto.CheckUpdateResult -import com.v2ray.ang.extension.toast -import com.v2ray.ang.extension.toastError -import com.v2ray.ang.extension.toastSuccess -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.handler.UpdateCheckerManager -import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.Utils -import kotlinx.coroutines.launch +import com.v2ray.ang.viewmodel.CheckUpdateViewModel class CheckUpdateActivity : BaseComponentActivity() { + private val viewModel: CheckUpdateViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } @Composable override fun ScreenContent() { - CheckUpdateScreen(onBackClick = { finish() }) + CheckUpdateScreen(viewModel = viewModel, onBackClick = { finish() }) } } @Composable -fun CheckUpdateScreen(onBackClick: () -> Unit) { +fun CheckUpdateScreen( + viewModel: CheckUpdateViewModel, + onBackClick: () -> Unit +) { val context = LocalContext.current - val scope = rememberCoroutineScope() - val snackbar = LocalAppSnackbar.current - var isLoading by remember { mutableStateOf(false) } - var checkPreRelease by remember { - mutableStateOf(MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false)) - } - var showUpdateDialog by remember { mutableStateOf(false) } - var updateResult by remember { mutableStateOf(null) } + val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() + val checkPreRelease by viewModel.checkPreRelease.collectAsStateWithLifecycle() + val showUpdateDialog by viewModel.showUpdateDialog.collectAsStateWithLifecycle() + val updateResult by viewModel.updateResult.collectAsStateWithLifecycle() val versionText = "v${BuildConfig.VERSION_NAME} (${CoreNativeManager.getLibVersion()})" - fun checkForUpdates(includePreRelease: Boolean) { - snackbar.showInfo(context, (R.string.update_checking_for_update)) - isLoading = true - scope.launch { - try { - val result = UpdateCheckerManager.checkForUpdate(includePreRelease) - if (result.hasUpdate) { - updateResult = result - showUpdateDialog = true - } else { - snackbar.showSuccess(context, R.string.update_already_latest_version) - } - } catch (e: Exception) { - LogUtil.e(AppConfig.TAG, "Failed to check for updates: ${e.message}") - if (e.message == null) { - snackbar.showError(context, R.string.toast_failure) - } else { - snackbar.showError(e.message.orEmpty()) - } - } finally { - isLoading = false - } - } + LaunchedEffect(Unit) { + viewModel.checkForUpdates() } - LaunchedEffect(Unit) { checkForUpdates(checkPreRelease) } - Scaffold( contentWindowInsets = ScaffoldDefaults.contentWindowInsets, topBar = { @@ -117,15 +83,12 @@ fun CheckUpdateScreen(onBackClick: () -> Unit) { icon = painterResource(R.drawable.ic_source_code_24dp), title = stringResource(R.string.update_check_pre_release), checked = checkPreRelease, - onCheckedChange = { checked -> - checkPreRelease = checked - MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, checked) - } + onCheckedChange = { viewModel.toggleCheckPreRelease(it) } ) SettingsMenuItem( icon = painterResource(R.drawable.ic_check_update_24dp), title = stringResource(R.string.update_check_for_update), - onClick = { checkForUpdates(checkPreRelease) } + onClick = { viewModel.checkForUpdates() } ) VersionInfoBlock(versionText = versionText) } @@ -134,19 +97,19 @@ fun CheckUpdateScreen(onBackClick: () -> Unit) { if (showUpdateDialog && updateResult != null) { val result = updateResult!! AlertDialog( - onDismissRequest = { showUpdateDialog = false }, + onDismissRequest = { viewModel.dismissUpdateDialog() }, title = { Text(stringResource(R.string.update_new_version_found, result.latestVersion ?: "")) }, text = { Text(result.releaseNotes ?: "") }, confirmButton = { TextButton(onClick = { - showUpdateDialog = false + viewModel.dismissUpdateDialog() result.downloadUrl?.let { Utils.openUri(context, it) } }) { Text(stringResource(R.string.update_now)) } }, dismissButton = { - TextButton(onClick = { showUpdateDialog = false }) { + TextButton(onClick = { viewModel.dismissUpdateDialog() }) { Text(stringResource(android.R.string.cancel)) } }, diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/BaseViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/BaseViewModel.kt index fde1436a..3c8f4ba4 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/BaseViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/BaseViewModel.kt @@ -3,6 +3,7 @@ package com.v2ray.ang.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.v2ray.ang.AngApplication +import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastSuccess import kotlinx.coroutines.channels.Channel @@ -26,6 +27,20 @@ abstract class BaseViewModel : ViewModel() { protected val _viewModelEvent = Channel() val viewModelEvent = _viewModelEvent.receiveAsFlow() + /** + * Send neutral toast event (Resource ID). + */ + fun toast(resId: Int) { + AngApplication.application.toast(resId) + } + + /** + * Send neutral toast event (String). + */ + fun toast(message: String) { + AngApplication.application.toast(message) + } + /** * Send success toast event (Resource ID). */ diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/CheckUpdateViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/CheckUpdateViewModel.kt new file mode 100644 index 00000000..17a2f3d7 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/CheckUpdateViewModel.kt @@ -0,0 +1,56 @@ +package com.v2ray.ang.viewmodel + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.dto.CheckUpdateResult +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.UpdateCheckerManager +import com.v2ray.ang.util.LogUtil +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class CheckUpdateViewModel : BaseViewModel() { + + private val _checkPreRelease = MutableStateFlow( + MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false) + ) + val checkPreRelease: StateFlow = _checkPreRelease.asStateFlow() + + private val _updateResult = MutableStateFlow(null) + val updateResult: StateFlow = _updateResult.asStateFlow() + + private val _showUpdateDialog = MutableStateFlow(false) + val showUpdateDialog: StateFlow = _showUpdateDialog.asStateFlow() + + fun toggleCheckPreRelease(enabled: Boolean) { + _checkPreRelease.value = enabled + MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, enabled) + } + + fun checkForUpdates() { + launchLoading { + toast(R.string.update_checking_for_update) + try { + val result = UpdateCheckerManager.checkForUpdate(_checkPreRelease.value) + if (result.hasUpdate) { + _updateResult.value = result + _showUpdateDialog.value = true + } else { + toastSuccess(R.string.update_already_latest_version) + } + } catch (e: Exception) { + LogUtil.e(AppConfig.TAG, "Failed to check for updates: ${e.message}") + if (e.message == null) { + toastError(R.string.toast_failure) + } else { + toastError(e.message.orEmpty()) + } + } + } + } + + fun dismissUpdateDialog() { + _showUpdateDialog.value = false + } +}