From 90ae44e5329e3d8e82f7cfb23f003c0bfda3a052 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:26:07 +0800 Subject: [PATCH] Refactor SettingsActivity to MVVM architecture --- .../java/com/v2ray/ang/ui/SettingsActivity.kt | 73 ++++++------------- .../com/v2ray/ang/ui/UserAssetActivity.kt | 2 - .../v2ray/ang/viewmodel/SettingsViewModel.kt | 58 +++++++++++++++ 3 files changed, 81 insertions(+), 52 deletions(-) create mode 100644 V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt index 0e4f8820..fb23feff 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt @@ -1,7 +1,7 @@ package com.v2ray.ang.ui -import android.content.Context import android.os.Bundle +import androidx.activity.viewModels import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -10,19 +10,17 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -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.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.R @@ -34,18 +32,17 @@ import com.v2ray.ang.compose.SettingsMenuItem import com.v2ray.ang.compose.SettingsSwitchItem import com.v2ray.ang.compose.ThemeManager import com.v2ray.ang.compose.verticalScrollbar -import com.v2ray.ang.extension.toastError import com.v2ray.ang.handler.MmkvManager.rememberMmkvBool import com.v2ray.ang.handler.MmkvManager.rememberMmkvString import com.v2ray.ang.handler.SettingsChangeManager import com.v2ray.ang.root.RootManager import com.v2ray.ang.util.Utils -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import com.v2ray.ang.viewmodel.SettingsViewModel class SettingsActivity : BaseComponentActivity() { + private val viewModel: SettingsViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } @@ -53,6 +50,7 @@ class SettingsActivity : BaseComponentActivity() { @Composable override fun ScreenContent() { SettingsScreen( + viewModel = viewModel, onBackClick = { finish() }, onModeHelpClicked = { Utils.openUri(this, AppConfig.APP_WIKI_MODE) } ) @@ -62,20 +60,12 @@ class SettingsActivity : BaseComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen( + viewModel: SettingsViewModel, onBackClick: () -> Unit, onModeHelpClicked: () -> Unit ) { - val scope = rememberCoroutineScope() - val context = LocalContext.current val scrollState = rememberScrollState() - - suspend fun checkAndRequestRoot(context: Context): Boolean { - val hasRoot = withContext(Dispatchers.IO) { RootManager.refresh() } - if (!hasRoot) { - context.toastError(R.string.toast_root_required) - } - return hasRoot - } + val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() var localDns by rememberMmkvBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false) var fakeDns by rememberMmkvBool(AppConfig.PREF_FAKE_DNS_ENABLED, false) @@ -171,30 +161,13 @@ fun SettingsScreen( val modeEntries = stringArrayResource(R.array.mode_entries).toList() val modeValues = stringArrayResource(R.array.mode_value).toList() - fun updateObservatoryDuration(value: String, onValid: (String) -> Unit) { - val duration = value.trim() - if (AppConfig.OBSERVATORY_DURATION_PATTERN.matches(duration)) { - onValid(duration) - } else { - context.toastError(R.string.toast_invalid_observatory_duration) - } - } - - fun updateObservatorySampling(value: String) { - val sampling = value.trim().toIntOrNull()?.takeIf { it > 0 } - if (sampling != null) { - observatoryLeastLoadSampling = sampling.toString() - } else { - context.toastError(R.string.toast_invalid_observatory_sampling) - } - } - Scaffold( contentWindowInsets = ScaffoldDefaults.contentWindowInsets, topBar = { AppTopBar( title = stringResource(R.string.title_settings), - onBackClick = onBackClick + onBackClick = onBackClick, + isLoading = isLoading ) } ) { innerPadding -> @@ -520,7 +493,7 @@ fun SettingsScreen( title = stringResource(R.string.title_pref_observatory_least_ping_interval), value = observatoryLeastPingInterval, onValueChanged = { - updateObservatoryDuration(it) { value -> + viewModel.validateObservatoryDuration(it)?.let { value -> observatoryLeastPingInterval = value } } @@ -529,7 +502,7 @@ fun SettingsScreen( title = stringResource(R.string.title_pref_observatory_least_load_interval), value = observatoryLeastLoadInterval, onValueChanged = { - updateObservatoryDuration(it) { value -> + viewModel.validateObservatoryDuration(it)?.let { value -> observatoryLeastLoadInterval = value } } @@ -545,13 +518,17 @@ fun SettingsScreen( title = stringResource(R.string.title_pref_observatory_least_load_sampling), value = observatoryLeastLoadSampling, keyboardNumber = true, - onValueChanged = { updateObservatorySampling(it) } + onValueChanged = { + viewModel.validateObservatorySampling(it)?.let { value -> + observatoryLeastLoadSampling = value + } + } ) SettingsEditItem( title = stringResource(R.string.title_pref_observatory_least_load_timeout), value = observatoryLeastLoadTimeout, onValueChanged = { - updateObservatoryDuration(it) { value -> + viewModel.validateObservatoryDuration(it)?.let { value -> observatoryLeastLoadTimeout = value } } @@ -611,10 +588,8 @@ fun SettingsScreen( checked = enableRootMode, onCheckedChange = { newValue -> if (newValue && !RootManager.cachedRoot()) { - scope.launch { - if (checkAndRequestRoot(context)) { - enableRootMode = true - } + viewModel.checkAndRequestRoot { + enableRootMode = true } } else { enableRootMode = newValue @@ -627,10 +602,8 @@ fun SettingsScreen( checked = lanSharing, onCheckedChange = { newValue -> if (newValue && !RootManager.cachedRoot()) { - scope.launch { - if (checkAndRequestRoot(context)) { - lanSharing = true - } + viewModel.checkAndRequestRoot { + lanSharing = true } } else { lanSharing = newValue diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt index 089de061..4be681cb 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt @@ -1,6 +1,5 @@ package com.v2ray.ang.ui -import android.annotation.SuppressLint import android.content.Intent import android.net.Uri import android.os.Bundle @@ -234,7 +233,6 @@ class UserAssetActivity : HelperBaseComponentActivity() { } } - @SuppressLint("NotifyDataSetChanged") fun refreshData() { viewModel.reload(getGeoFilesSources()) refreshTrigger.value++ diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt new file mode 100644 index 00000000..5512a8d3 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt @@ -0,0 +1,58 @@ +package com.v2ray.ang.viewmodel + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.root.RootManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext + +class SettingsViewModel : BaseViewModel() { + + /** + * Checks for root access and requests it if necessary. + * Updates [isLoading] during the process. + */ + fun checkAndRequestRoot(onSuccess: () -> Unit) { + launchLoading { + val hasRoot = withContext(Dispatchers.IO) { + RootManager.refresh() + } + if (hasRoot) { + onSuccess() + } else { + toastError(R.string.toast_root_required) + } + } + } + + /** + * Validates if the given string is a valid observatory duration. + * Shows error toast if invalid. + * @return The trimmed value if valid, null otherwise. + */ + fun validateObservatoryDuration(value: String): String? { + val duration = value.trim() + return if (AppConfig.OBSERVATORY_DURATION_PATTERN.matches(duration)) { + duration + } else { + toastError(R.string.toast_invalid_observatory_duration) + null + } + } + + /** + * Validates if the given string is a valid observatory sampling value. + * Shows error toast if invalid. + * @return The value if valid, null otherwise. + */ + fun validateObservatorySampling(value: String): String? { + val sampling = value.trim().toIntOrNull()?.takeIf { it > 0 } + return if (sampling != null) { + sampling.toString() + } else { + toastError(R.string.toast_invalid_observatory_sampling) + null + } + } +}