Refactor BackupActivity to MVVM architecture
- Create BaseViewModel to encapsulate common UI state (isLoading) and logic (toasts, finishing activity). - Implement ViewModelEvent and BaseViewModelEvent for standardized UI communication via SideEffects. - Move core business logic (I/O, Zip, WebDAV) from BackupActivity to BackupViewModel. - Standardize code naming conventions and update documentation to English. - Improve Activity purity by delegating state management and background tasks to the ViewModel.
This commit is contained in:
1 parent
70352cc341
commit
aaf071053b
4 files changed
+341
-201
No files matched your search
@@ -1,7 +1,9 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
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
|
||||
@@ -23,10 +25,10 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.tencent.mmkv.MMKV
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.WEBDAV_BACKUP_FILE_NAME
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.compose.AppTopBar
|
||||
@@ -37,25 +39,18 @@ import com.v2ray.ang.compose.SettingsMenuItem
|
||||
import com.v2ray.ang.dto.entities.WebDavConfig
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsChangeManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.WebDavManager
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.ZipUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import com.v2ray.ang.viewmodel.BackupViewModel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class BackupActivity : HelperBaseComponentActivity() {
|
||||
|
||||
private val isLoadingState = MutableStateFlow(false)
|
||||
private val webDavConfigState = MutableStateFlow(MmkvManager.decodeWebDavConfig())
|
||||
private val viewModel: BackupViewModel by viewModels()
|
||||
|
||||
private val configBackupOptions: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.config_backup_options)
|
||||
@@ -63,96 +58,91 @@ class BackupActivity : HelperBaseComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
observeViewModel()
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.viewModelEvent.collect { event ->
|
||||
when (event) {
|
||||
is BackupViewModel.BackupViewModelEvent.ShareFile -> {
|
||||
handleShareFile(event.filePath)
|
||||
}
|
||||
|
||||
is BackupViewModel.BackupViewModelEvent.ExportLocal -> {
|
||||
handleExportLocal(event.cachePath, event.targetUri)
|
||||
}
|
||||
|
||||
is BackupViewModel.BackupViewModelEvent.RestoreSuccess -> {
|
||||
SettingsManager.initApp(this@BackupActivity)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent() {
|
||||
BackupScreen(
|
||||
isLoadingState = isLoadingState,
|
||||
webDavConfigState = webDavConfigState,
|
||||
isLoadingState = viewModel.isLoading,
|
||||
webDavConfigState = viewModel.webDavConfig,
|
||||
backupOptions = configBackupOptions.toList(),
|
||||
onBackupOptionSelected = { which ->
|
||||
when (which) {
|
||||
0 -> backupViaLocal()
|
||||
1 -> backupViaWebDav()
|
||||
1 -> viewModel.backupViaWebDav(cacheDir, getString(R.string.app_name))
|
||||
}
|
||||
},
|
||||
onShareClick = { shareBackup() },
|
||||
onShareClick = { viewModel.shareBackup(cacheDir, getString(R.string.app_name)) },
|
||||
restoreOptions = configBackupOptions.toList(),
|
||||
onRestoreOptionSelected = { which ->
|
||||
when (which) {
|
||||
0 -> restoreViaLocal()
|
||||
1 -> restoreViaWebDav()
|
||||
1 -> viewModel.restoreViaWebDav(cacheDir)
|
||||
}
|
||||
},
|
||||
onWebDavSave = { config -> saveWebDav(config) },
|
||||
onWebDavSave = { config -> viewModel.saveWebDavConfig(config) },
|
||||
onBackClick = { finish() }
|
||||
)
|
||||
}
|
||||
|
||||
private fun shareBackup() {
|
||||
val ret = backupConfigurationToCache()
|
||||
if (ret.first) {
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_SEND).setType("application/zip")
|
||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.putExtra(
|
||||
Intent.EXTRA_STREAM,
|
||||
FileProvider.getUriForFile(
|
||||
this,
|
||||
BuildConfig.APPLICATION_ID + ".cache",
|
||||
File(ret.second)
|
||||
)
|
||||
),
|
||||
getString(R.string.title_configuration_share)
|
||||
)
|
||||
private fun handleShareFile(filePath: String) {
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_SEND).setType("application/zip")
|
||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.putExtra(
|
||||
Intent.EXTRA_STREAM,
|
||||
FileProvider.getUriForFile(
|
||||
this,
|
||||
BuildConfig.APPLICATION_ID + ".cache",
|
||||
File(filePath)
|
||||
)
|
||||
),
|
||||
getString(R.string.title_configuration_share)
|
||||
)
|
||||
} else {
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleExportLocal(cachePath: String, targetUri: Uri) {
|
||||
try {
|
||||
contentResolver.openOutputStream(targetUri)?.use { output ->
|
||||
File(cachePath).inputStream().use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
File(cachePath).delete()
|
||||
toastSuccess(R.string.toast_success)
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to copy backup to Uri", e)
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup configuration to cache directory
|
||||
* Returns Pair<success, zipFilePath>
|
||||
*/
|
||||
private fun backupConfigurationToCache(): Pair<Boolean, String> {
|
||||
val dateFormatted = SimpleDateFormat(
|
||||
"yyyy-MM-dd-HH-mm-ss",
|
||||
Locale.getDefault()
|
||||
).format(System.currentTimeMillis())
|
||||
val folderName = "${getString(R.string.app_name)}_${dateFormatted}"
|
||||
val backupDir = this.cacheDir.absolutePath + "/$folderName"
|
||||
val outputZipFilePath = "${this.cacheDir.absolutePath}/$folderName.zip"
|
||||
|
||||
val count = MMKV.backupAllToDirectory(backupDir)
|
||||
if (count <= 0) {
|
||||
return Pair(false, "")
|
||||
}
|
||||
|
||||
if (ZipUtil.zipFromFolder(backupDir, outputZipFilePath)) {
|
||||
return Pair(true, outputZipFilePath)
|
||||
} else {
|
||||
return Pair(false, "")
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreConfiguration(zipFile: File): Boolean {
|
||||
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
|
||||
|
||||
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val count = MMKV.restoreAllFromDirectory(backupDir)
|
||||
SettingsChangeManager.makeSetupGroupTab()
|
||||
SettingsChangeManager.makeRestartService()
|
||||
|
||||
SettingsManager.initApp(this)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
private fun backupViaLocal() {
|
||||
val dateFormatted = SimpleDateFormat(
|
||||
"yyyy-MM-dd-HH-mm-ss",
|
||||
@@ -162,25 +152,7 @@ class BackupActivity : HelperBaseComponentActivity() {
|
||||
|
||||
launchCreateDocument(defaultFileName) { uri ->
|
||||
if (uri != null) {
|
||||
try {
|
||||
val ret = backupConfigurationToCache()
|
||||
if (ret.first) {
|
||||
// Copy the cached zip file to user-selected location
|
||||
contentResolver.openOutputStream(uri)?.use { output ->
|
||||
File(ret.second).inputStream().use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
// Clean up cache file
|
||||
File(ret.second).delete()
|
||||
toastSuccess(R.string.toast_success)
|
||||
} else {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to backup configuration", e)
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
viewModel.prepareBackupForUri(cacheDir, getString(R.string.app_name), uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,124 +164,19 @@ class BackupActivity : HelperBaseComponentActivity() {
|
||||
}
|
||||
try {
|
||||
val targetFile =
|
||||
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
|
||||
File(cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
|
||||
contentResolver.openInputStream(uri).use { input ->
|
||||
targetFile.outputStream().use { fileOut ->
|
||||
input?.copyTo(fileOut)
|
||||
}
|
||||
}
|
||||
if (restoreConfiguration(targetFile)) {
|
||||
toastSuccess(R.string.toast_success)
|
||||
} else {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
viewModel.restoreConfiguration(cacheDir, targetFile)
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Error during file restore", e)
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun backupViaWebDav() {
|
||||
val saved = MmkvManager.decodeWebDavConfig()
|
||||
if (saved == null || saved.baseUrl.isEmpty()) {
|
||||
toastError(R.string.title_webdav_config_setting_unknown)
|
||||
return
|
||||
}
|
||||
|
||||
isLoadingState.value = true
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
var tempFile: File? = null
|
||||
try {
|
||||
val ret = backupConfigurationToCache()
|
||||
if (!ret.first) {
|
||||
withContext(Dispatchers.Main) {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
tempFile = File(ret.second)
|
||||
WebDavManager.init(saved)
|
||||
|
||||
val ok = try {
|
||||
WebDavManager.uploadFile(tempFile, WEBDAV_BACKUP_FILE_NAME)
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "WebDAV upload error", e)
|
||||
false
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (ok) toastSuccess(R.string.toast_success) else toastError(R.string.toast_failure)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "WebDAV backup error", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
tempFile?.delete()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
isLoadingState.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreViaWebDav() {
|
||||
val saved = MmkvManager.decodeWebDavConfig()
|
||||
if (saved == null || saved.baseUrl.isEmpty()) {
|
||||
toastError(R.string.title_webdav_config_setting_unknown)
|
||||
return
|
||||
}
|
||||
|
||||
isLoadingState.value = true
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
var target: File? = null
|
||||
try {
|
||||
target = File(cacheDir, "download_${System.currentTimeMillis()}.zip")
|
||||
WebDavManager.init(saved)
|
||||
val ok = WebDavManager.downloadFile(WEBDAV_BACKUP_FILE_NAME, target)
|
||||
if (!ok) {
|
||||
withContext(Dispatchers.Main) {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
val restored = restoreConfiguration(target)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (restored) {
|
||||
toastSuccess(R.string.toast_success)
|
||||
} else {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "WebDAV download error", e)
|
||||
withContext(Dispatchers.Main) { toastError(R.string.toast_failure) }
|
||||
} finally {
|
||||
try {
|
||||
target?.delete()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
isLoadingState.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveWebDav(config: WebDavConfig){
|
||||
MmkvManager.encodeWebDavConfig(config)
|
||||
webDavConfigState.value = config
|
||||
toastSuccess(R.string.toast_success)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.v2ray.ang.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.WEBDAV_BACKUP_FILE_NAME
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.entities.WebDavConfig
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsChangeManager
|
||||
import com.v2ray.ang.handler.WebDavManager
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.ZipUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class BackupViewModel : BaseViewModel() {
|
||||
|
||||
private val _webDavConfig = MutableStateFlow(MmkvManager.decodeWebDavConfig())
|
||||
val webDavConfig: StateFlow<WebDavConfig?> = _webDavConfig.asStateFlow()
|
||||
|
||||
sealed interface BackupViewModelEvent : ViewModelEvent {
|
||||
data class ShareFile(val filePath: String) : BackupViewModelEvent
|
||||
data class ExportLocal(val cachePath: String, val targetUri: Uri) : BackupViewModelEvent
|
||||
object RestoreSuccess : BackupViewModelEvent
|
||||
}
|
||||
|
||||
fun saveWebDavConfig(config: WebDavConfig) {
|
||||
MmkvManager.encodeWebDavConfig(config)
|
||||
_webDavConfig.value = config
|
||||
toastSuccess(R.string.toast_success)
|
||||
}
|
||||
|
||||
fun shareBackup(cacheDir: File, appName: String) {
|
||||
launchLoading {
|
||||
val ret = backupConfigurationToCache(cacheDir, appName)
|
||||
if (ret.first) {
|
||||
_viewModelEvent.send(BackupViewModelEvent.ShareFile(ret.second))
|
||||
} else {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareBackupForUri(cacheDir: File, appName: String, targetUri: Uri) {
|
||||
launchLoading {
|
||||
val ret = backupConfigurationToCache(cacheDir, appName)
|
||||
if (ret.first) {
|
||||
_viewModelEvent.send(BackupViewModelEvent.ExportLocal(ret.second, targetUri))
|
||||
} else {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreConfiguration(cacheDir: File, zipFile: File) {
|
||||
launchLoading {
|
||||
val success = performRestore(cacheDir, zipFile)
|
||||
if (success) {
|
||||
toastSuccess(R.string.toast_success)
|
||||
_viewModelEvent.send(BackupViewModelEvent.RestoreSuccess)
|
||||
} else {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun backupViaWebDav(cacheDir: File, appName: String) {
|
||||
val config = _webDavConfig.value
|
||||
if (config == null || (config.baseUrl.isEmpty())) {
|
||||
toastError(R.string.title_webdav_config_setting_unknown)
|
||||
return
|
||||
}
|
||||
|
||||
launchLoading {
|
||||
var tempFile: File? = null
|
||||
try {
|
||||
val ret = backupConfigurationToCache(cacheDir, appName)
|
||||
if (!ret.first) {
|
||||
toastError(R.string.toast_failure)
|
||||
return@launchLoading
|
||||
}
|
||||
|
||||
tempFile = File(ret.second)
|
||||
WebDavManager.init(config)
|
||||
|
||||
val ok = try {
|
||||
WebDavManager.uploadFile(tempFile, WEBDAV_BACKUP_FILE_NAME)
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "WebDAV upload error", e)
|
||||
false
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
toastSuccess(R.string.toast_success)
|
||||
} else {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "WebDAV backup error", e)
|
||||
toastError(R.string.toast_failure)
|
||||
} finally {
|
||||
tempFile?.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreViaWebDav(cacheDir: File) {
|
||||
val config = _webDavConfig.value
|
||||
if (config == null || (config.baseUrl.isEmpty())) {
|
||||
toastError(R.string.title_webdav_config_setting_unknown)
|
||||
return
|
||||
}
|
||||
|
||||
launchLoading {
|
||||
var target: File? = null
|
||||
try {
|
||||
target = File(cacheDir, "download_${System.currentTimeMillis()}.zip")
|
||||
WebDavManager.init(config)
|
||||
val ok = WebDavManager.downloadFile(WEBDAV_BACKUP_FILE_NAME, target)
|
||||
if (!ok) {
|
||||
toastError(R.string.toast_failure)
|
||||
return@launchLoading
|
||||
}
|
||||
|
||||
if (performRestore(cacheDir, target)) {
|
||||
toastSuccess(R.string.toast_success)
|
||||
_viewModelEvent.send(BackupViewModelEvent.RestoreSuccess)
|
||||
} else {
|
||||
toastError(R.string.toast_failure)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "WebDAV download error", e)
|
||||
toastError(R.string.toast_failure)
|
||||
} finally {
|
||||
target?.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun backupConfigurationToCache(cacheDir: File, appName: String): Pair<Boolean, String> {
|
||||
val dateFormatted = SimpleDateFormat(
|
||||
"yyyy-MM-dd-HH-mm-ss",
|
||||
Locale.getDefault()
|
||||
).format(System.currentTimeMillis())
|
||||
val folderName = "${appName}_$dateFormatted"
|
||||
val backupDir = cacheDir.absolutePath + "/$folderName"
|
||||
val outputZipFilePath = "${cacheDir.absolutePath}/$folderName.zip"
|
||||
|
||||
val count = MMKV.backupAllToDirectory(backupDir)
|
||||
if (count <= 0) {
|
||||
return Pair(false, "")
|
||||
}
|
||||
|
||||
return if (ZipUtil.zipFromFolder(backupDir, outputZipFilePath)) {
|
||||
Pair(true, outputZipFilePath)
|
||||
} else {
|
||||
Pair(false, "")
|
||||
}
|
||||
}
|
||||
|
||||
private fun performRestore(cacheDir: File, zipFile: File): Boolean {
|
||||
val backupDir = cacheDir.absolutePath + "/${System.currentTimeMillis()}"
|
||||
|
||||
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val count = MMKV.restoreAllFromDirectory(backupDir)
|
||||
SettingsChangeManager.makeSetupGroupTab()
|
||||
SettingsChangeManager.makeRestartService()
|
||||
|
||||
return count > 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.v2ray.ang.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Base ViewModel that encapsulates common UI event handling logic.
|
||||
*/
|
||||
abstract class BaseViewModel : ViewModel() {
|
||||
|
||||
@Suppress("PropertyName")
|
||||
protected val _isLoading = MutableStateFlow(false)
|
||||
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
||||
|
||||
@Suppress("PropertyName")
|
||||
protected val _viewModelEvent = Channel<ViewModelEvent>()
|
||||
val viewModelEvent = _viewModelEvent.receiveAsFlow()
|
||||
|
||||
/**
|
||||
* Send success toast event (Resource ID).
|
||||
*/
|
||||
fun toastSuccess(resId: Int) {
|
||||
AngApplication.application.toastSuccess(resId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send success toast event (String).
|
||||
*/
|
||||
fun toastSuccess(message: String) {
|
||||
AngApplication.application.toastSuccess(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send error toast event (Resource ID).
|
||||
*/
|
||||
fun toastError(resId: Int) {
|
||||
AngApplication.application.toastError(resId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send error toast event (String).
|
||||
*/
|
||||
fun toastError(message: String) {
|
||||
AngApplication.application.toastError(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send finish activity event.
|
||||
*/
|
||||
fun finishActivity() {
|
||||
viewModelScope.launch {
|
||||
_viewModelEvent.send(BaseViewModelEvent.FinishActivity)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method: execute task in coroutine and automatically manage isLoading state.
|
||||
*/
|
||||
protected fun launchLoading(block: suspend CoroutineScope.() -> Unit) {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.v2ray.ang.viewmodel
|
||||
|
||||
/**
|
||||
* Base interface for ViewModel UI events.
|
||||
*/
|
||||
sealed interface ViewModelEvent
|
||||
|
||||
/**
|
||||
* Common UI events for all ViewModels.
|
||||
*/
|
||||
sealed interface BaseViewModelEvent : ViewModelEvent {
|
||||
object FinishActivity : BaseViewModelEvent
|
||||
}
|
||||
Reference in new issue
Block a user