mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-21 15:01:03 +00:00
perf(android): reduce tab-switch CPU churn
This commit is contained in:
@@ -75,7 +75,7 @@ class ChatController(
|
||||
fun load(sessionKey: String) {
|
||||
val key = sessionKey.trim().ifEmpty { "main" }
|
||||
_sessionKey.value = key
|
||||
scope.launch { bootstrap(forceHealth = true) }
|
||||
scope.launch { bootstrap(forceHealth = true, refreshSessions = true) }
|
||||
}
|
||||
|
||||
fun applyMainSessionKey(mainSessionKey: String) {
|
||||
@@ -84,11 +84,11 @@ class ChatController(
|
||||
if (_sessionKey.value == trimmed) return
|
||||
if (_sessionKey.value != "main") return
|
||||
_sessionKey.value = trimmed
|
||||
scope.launch { bootstrap(forceHealth = true) }
|
||||
scope.launch { bootstrap(forceHealth = true, refreshSessions = true) }
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
scope.launch { bootstrap(forceHealth = true) }
|
||||
scope.launch { bootstrap(forceHealth = true, refreshSessions = true) }
|
||||
}
|
||||
|
||||
fun refreshSessions(limit: Int? = null) {
|
||||
@@ -106,7 +106,9 @@ class ChatController(
|
||||
if (key.isEmpty()) return
|
||||
if (key == _sessionKey.value) return
|
||||
_sessionKey.value = key
|
||||
scope.launch { bootstrap(forceHealth = true) }
|
||||
// Keep the thread switch path lean: history + health are needed immediately,
|
||||
// but the session list is usually unchanged and can refresh on explicit pull-to-refresh.
|
||||
scope.launch { bootstrap(forceHealth = true, refreshSessions = false) }
|
||||
}
|
||||
|
||||
fun sendMessage(
|
||||
@@ -249,7 +251,7 @@ class ChatController(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun bootstrap(forceHealth: Boolean) {
|
||||
private suspend fun bootstrap(forceHealth: Boolean, refreshSessions: Boolean) {
|
||||
_errorText.value = null
|
||||
_healthOk.value = false
|
||||
clearPendingRuns()
|
||||
@@ -271,7 +273,9 @@ class ChatController(
|
||||
history.thinkingLevel?.trim()?.takeIf { it.isNotEmpty() }?.let { _thinkingLevel.value = it }
|
||||
|
||||
pollHealthIfNeeded(force = forceHealth)
|
||||
fetchSessions(limit = 50)
|
||||
if (refreshSessions) {
|
||||
fetchSessions(limit = 50)
|
||||
}
|
||||
} catch (err: Throwable) {
|
||||
_errorText.value = err.message
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import ai.openclaw.app.MainViewModel
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun CanvasScreen(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
fun CanvasScreen(viewModel: MainViewModel, visible: Boolean, modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
val isDebuggable = (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||
val webViewRef = remember { mutableStateOf<WebView?>(null) }
|
||||
@@ -45,6 +45,7 @@ fun CanvasScreen(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
modifier = modifier,
|
||||
factory = {
|
||||
WebView(context).apply {
|
||||
visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
|
||||
@@ -127,6 +128,16 @@ fun CanvasScreen(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
webViewRef.value = this
|
||||
}
|
||||
},
|
||||
update = { webView ->
|
||||
webView.visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
||||
if (visible) {
|
||||
webView.resumeTimers()
|
||||
webView.onResume()
|
||||
} else {
|
||||
webView.onPause()
|
||||
webView.pauseTimers()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -68,10 +70,19 @@ private enum class StatusVisual {
|
||||
@Composable
|
||||
fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
var activeTab by rememberSaveable { mutableStateOf(HomeTab.Connect) }
|
||||
var chatTabStarted by rememberSaveable { mutableStateOf(false) }
|
||||
var screenTabStarted by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
// Stop TTS when user navigates away from voice tab
|
||||
// Stop TTS when user navigates away from voice tab, and lazily keep the Chat/Screen tabs
|
||||
// alive after the first visit so repeated tab switches do not rebuild their UI trees.
|
||||
LaunchedEffect(activeTab) {
|
||||
viewModel.setVoiceScreenActive(activeTab == HomeTab.Voice)
|
||||
if (activeTab == HomeTab.Chat) {
|
||||
chatTabStarted = true
|
||||
}
|
||||
if (activeTab == HomeTab.Screen) {
|
||||
screenTabStarted = true
|
||||
}
|
||||
}
|
||||
|
||||
val statusText by viewModel.statusText.collectAsState()
|
||||
@@ -120,11 +131,35 @@ fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier)
|
||||
.consumeWindowInsets(innerPadding)
|
||||
.background(mobileBackgroundGradient),
|
||||
) {
|
||||
if (chatTabStarted) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.alpha(if (activeTab == HomeTab.Chat) 1f else 0f)
|
||||
.zIndex(if (activeTab == HomeTab.Chat) 1f else 0f),
|
||||
) {
|
||||
ChatSheet(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
if (screenTabStarted) {
|
||||
ScreenTabScreen(
|
||||
viewModel = viewModel,
|
||||
visible = activeTab == HomeTab.Screen,
|
||||
modifier =
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.alpha(if (activeTab == HomeTab.Screen) 1f else 0f)
|
||||
.zIndex(if (activeTab == HomeTab.Screen) 1f else 0f),
|
||||
)
|
||||
}
|
||||
|
||||
when (activeTab) {
|
||||
HomeTab.Connect -> ConnectTabScreen(viewModel = viewModel)
|
||||
HomeTab.Chat -> ChatSheet(viewModel = viewModel)
|
||||
HomeTab.Chat -> if (!chatTabStarted) ChatSheet(viewModel = viewModel)
|
||||
HomeTab.Voice -> VoiceTabScreen(viewModel = viewModel)
|
||||
HomeTab.Screen -> ScreenTabScreen(viewModel = viewModel)
|
||||
HomeTab.Screen -> Unit
|
||||
HomeTab.Settings -> SettingsSheet(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
@@ -132,16 +167,19 @@ fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenTabScreen(viewModel: MainViewModel) {
|
||||
private fun ScreenTabScreen(viewModel: MainViewModel, visible: Boolean, modifier: Modifier = Modifier) {
|
||||
val isConnected by viewModel.isConnected.collectAsState()
|
||||
LaunchedEffect(isConnected) {
|
||||
if (isConnected) {
|
||||
var refreshedForCurrentConnection by rememberSaveable(isConnected) { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(isConnected, visible, refreshedForCurrentConnection) {
|
||||
if (visible && isConnected && !refreshedForCurrentConnection) {
|
||||
viewModel.refreshHomeCanvasOverviewIfConnected()
|
||||
refreshedForCurrentConnection = true
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
CanvasScreen(viewModel = viewModel, modifier = Modifier.fillMaxSize())
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
CanvasScreen(viewModel = viewModel, visible = visible, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,6 @@ fun ChatSheetContent(viewModel: MainViewModel) {
|
||||
|
||||
LaunchedEffect(mainSessionKey) {
|
||||
viewModel.loadChat(mainSessionKey)
|
||||
viewModel.refreshChatSessions(limit = 200)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
Reference in New Issue
Block a user