perf(#281): fix PERF-13/14/15/16 — main-thread settings, 100Hz polling, leaked scope, WS disconnect (#359)

* perf(#281): fix PERF-13/14/15/16 — main-thread settings load, 100Hz polling, leaked scope, WS disconnect

* fix(#281): fix SettingsViewModelTest failures from async init dispatcher

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-06-25 20:39:52 +03:00
1 parent 9297413b2a
commit 3399880621
5 files changed
+81 -46

No files matched your search

@@ -10,6 +10,7 @@ import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.theme.BottomNavDisplayMode
import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.theme.ThemePreset
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -38,47 +39,57 @@ data class SettingsUiState(
val bottomNavDisplayMode: BottomNavDisplayMode = BottomNavDisplayMode.ICON_AND_TEXT,
)
class SettingsViewModel : ViewModel() {
class SettingsViewModel(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : ViewModel() {
private val _uiState = MutableStateFlow(SettingsUiState())
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
init {
loadSettings()
viewModelScope.launch(ioDispatcher) {
loadSettings()
}
}
private fun loadSettings() {
private suspend fun loadSettings() {
val selectedId = AuthManager.getSelectedProfileId()
val host = AuthManager.getHost()
val port = AuthManager.getPort().toString()
val token = AuthManager.getToken() ?: ""
val autoReconnect = AuthManager.isAutoReconnect()
val themePreference = AuthManager.getThemePreference()
val useDynamicColors = AuthManager.isUseDynamicColors()
val themePreset = AuthManager.getThemePreset()
val selectedNavItems = AuthManager.getBottomNavItems()
val typingEffectEnabled = AuthManager.isTypingEffectEnabled()
val typingEffectDelayMs = AuthManager.getTypingEffectDelayMs()
val profiles = AuthManager.getConnectionProfiles()
val bottomNavDisplayMode = AuthManager.getBottomNavDisplayMode()
val renameProfileName =
profiles.firstOrNull { p -> p.id == selectedId }?.name ?: ""
_uiState.update {
it.copy(
host = AuthManager.getHost(),
port = AuthManager.getPort().toString(),
token = AuthManager.getToken() ?: "",
autoReconnect = AuthManager.isAutoReconnect(),
// B6 (Jun 18 2026, kanban t_86e9be9b): restore user's theme
// choice from persistent storage on init. Previously this slot
// was missing — always defaulted to ThemePreference.SYSTEM.
themePreference = AuthManager.getThemePreference(),
useDynamicColors = AuthManager.isUseDynamicColors(),
themePreset = AuthManager.getThemePreset(),
selectedNavItems = AuthManager.getBottomNavItems(),
typingEffectEnabled = AuthManager.isTypingEffectEnabled(),
typingEffectDelayMs = AuthManager.getTypingEffectDelayMs(),
profiles = AuthManager.getConnectionProfiles(),
host = host,
port = port,
token = token,
autoReconnect = autoReconnect,
themePreference = themePreference,
useDynamicColors = useDynamicColors,
themePreset = themePreset,
selectedNavItems = selectedNavItems,
typingEffectEnabled = typingEffectEnabled,
typingEffectDelayMs = typingEffectDelayMs,
profiles = profiles,
selectedProfileId = selectedId,
renameProfileName =
AuthManager
.getConnectionProfiles()
.firstOrNull { p ->
p.id == selectedId
}?.name ?: "",
bottomNavDisplayMode = AuthManager.getBottomNavDisplayMode(),
renameProfileName = renameProfileName,
bottomNavDisplayMode = bottomNavDisplayMode,
)
}
}
fun selectProfile(profileId: String?) {
AuthManager.setSelectedProfileId(profileId)
loadSettings()
viewModelScope.launch(ioDispatcher) { loadSettings() }
ApiClient.rebuild()
}
@@ -95,7 +106,7 @@ class SettingsViewModel : ViewModel() {
if (it.id == currentId) it.copy(name = newName) else it
}
AuthManager.saveConnectionProfiles(updatedProfiles)
loadSettings()
viewModelScope.launch(ioDispatcher) { loadSettings() }
}
fun deleteProfile(profileId: String) {
@@ -105,7 +116,7 @@ class SettingsViewModel : ViewModel() {
if (AuthManager.getSelectedProfileId() == profileId) {
AuthManager.setSelectedProfileId(null)
}
loadSettings()
viewModelScope.launch(ioDispatcher) { loadSettings() }
ApiClient.rebuild()
}
@@ -207,8 +218,10 @@ class SettingsViewModel : ViewModel() {
AuthManager.setTypingEffectDelayMs(state.typingEffectDelayMs)
ApiClient.rebuild()
loadSettings()
_uiState.update { it.copy(isSaved = true, testResult = null) }
viewModelScope.launch(ioDispatcher) {
loadSettings()
_uiState.update { it.copy(isSaved = true, testResult = null) }
}
}
fun testConnection() {
@@ -225,7 +238,7 @@ class SettingsViewModel : ViewModel() {
viewModelScope.launch {
val result =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
safeApiCall { ApiClient.hermesApi.getStatus() }
}
when (result) {