refactor: session/auth в MainViewModel (Этап 2 п.10, ч.2)

Верхнеуровневый session-жизненный цикл вынесен из AppScaffold в MainViewModel
(AndroidViewModel): держит session (StateFlow), login()=save+set,
logout()=ревокация app password + OfficeWarmup.clear + WebViewPool.dispose +
AuthStore.clear. AppScaffold читает vm.session.collectAsState(), forceLogout=
{vm.logout()}. Бонус: сессия переживает пересоздание Activity (поворот) —
раньше жила в remember и перечитывалась из AuthStore. UI-тумблеры (menuOpen,
sidebar/settings-флаги, pending* deep-link) осознанно оставлены в composable —
это чистое UI-состояние, не бизнес-логика.
This commit is contained in:
b-dev-mobile
2026-07-09 15:02:21 +00:00
parent 8c90e83cef
commit 47534ceb59
2 changed files with 47 additions and 23 deletions
@@ -50,12 +50,10 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import ru.forbion.f7cloud.mobile.BuildConfig
import ru.forbion.f7cloud.core.auth.AppLockStore
import ru.forbion.f7cloud.core.auth.AppPasswordRevoker
import ru.forbion.f7cloud.core.auth.AuthSession
import ru.forbion.f7cloud.core.auth.AuthStore
import ru.forbion.f7cloud.core.auth.AuthVerifier
import androidx.compose.runtime.collectAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import ru.forbion.f7cloud.core.network.LoginFlowClient
import ru.forbion.f7cloud.core.push.F7PushEvent
import ru.forbion.f7cloud.core.push.F7PushEventHub
@@ -68,12 +66,10 @@ import ru.forbion.f7cloud.core.designsystem.F7BottomBarActions
import ru.forbion.f7cloud.core.designsystem.F7BottomBarConfig
import ru.forbion.f7cloud.core.designsystem.F7MobileBottomBar
import ru.forbion.f7cloud.core.designsystem.F7Colors
import ru.forbion.f7cloud.core.designsystem.F7TextButton
import ru.forbion.f7cloud.core.designsystem.F7Theme
import ru.forbion.f7cloud.core.designsystem.f7SafeTopInsets
import ru.forbion.f7cloud.mobile.OfficeEditorActivity
import ru.forbion.f7cloud.mobile.qr.F7QrScannerActivity
import ru.forbion.f7cloud.mobile.OfficeWebViewPool
import ru.forbion.f7cloud.feature.files.OfficeWarmup
import ru.forbion.f7cloud.mobile.permissions.F7AppPermissions
import ru.forbion.f7cloud.mobile.permissions.F7PermissionRationaleDialog
@@ -95,8 +91,8 @@ fun AppScaffold(
onRequestRuntimePermissions: (onFinished: (() -> Unit)?) -> Unit = {},
) {
val context = LocalContext.current
val authStore = remember { AuthStore(context) }
var session by remember { mutableStateOf(authStore.load()) }
val mainVm: MainViewModel = viewModel()
val session by mainVm.session.collectAsState()
var activeTab by rememberSaveable(
saver = Saver(
save = { state -> state.value.name },
@@ -151,17 +147,7 @@ fun AppScaffold(
context.applicationContext.getSharedPreferences("f7_permissions", android.content.Context.MODE_PRIVATE)
}
val logoutScope = rememberCoroutineScope()
val forceLogout = {
// Best-effort ревокация app password на сервере ДО очистки локальной сессии.
session?.let { current ->
logoutScope.launch { AppPasswordRevoker.revoke(current) }
}
OfficeWarmup.clear()
OfficeWebViewPool.dispose()
authStore.clear()
session = null
}
val forceLogout = { mainVm.logout() }
LaunchedEffect(session?.serverUrl, session?.username) {
session?.let { OfficeWarmup.warm(it) }
}
@@ -174,10 +160,7 @@ fun AppScaffold(
F7Theme {
if (session == null) {
LoginScreen(onLogin = {
authStore.save(it)
session = it
})
LoginScreen(onLogin = { mainVm.login(it) })
return@F7Theme
}
@@ -0,0 +1,41 @@
package ru.forbion.f7cloud.mobile.ui
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.forbion.f7cloud.core.auth.AppPasswordRevoker
import ru.forbion.f7cloud.core.auth.AuthSession
import ru.forbion.f7cloud.core.auth.AuthStore
import ru.forbion.f7cloud.feature.files.OfficeWarmup
import ru.forbion.f7cloud.mobile.OfficeWebViewPool
/**
* Держит session/auth-жизненный цикл верхнего уровня (вынесено из AppScaffold).
* В ViewModel сессия переживает пересоздание Activity (поворот и т.п.) — раньше
* жила в remember и перечитывалась из AuthStore.
*/
class MainViewModel(app: Application) : AndroidViewModel(app) {
private val authStore = AuthStore(app)
private val _session = MutableStateFlow(authStore.load())
val session: StateFlow<AuthSession?> = _session.asStateFlow()
fun login(newSession: AuthSession) {
authStore.save(newSession)
_session.value = newSession
}
fun logout() {
// Best-effort ревокация app password на сервере ДО очистки локальной сессии.
_session.value?.let { current ->
viewModelScope.launch { runCatching { AppPasswordRevoker.revoke(current) } }
}
OfficeWarmup.clear()
OfficeWebViewPool.dispose()
authStore.clear()
_session.value = null
}
}