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.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
import ru.forbion.f7cloud.mobile.BuildConfig
import ru.forbion.f7cloud.core.auth.AppLockStore 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.AuthSession
import ru.forbion.f7cloud.core.auth.AuthStore import androidx.compose.runtime.collectAsState
import ru.forbion.f7cloud.core.auth.AuthVerifier import androidx.lifecycle.viewmodel.compose.viewModel
import ru.forbion.f7cloud.core.network.LoginFlowClient import ru.forbion.f7cloud.core.network.LoginFlowClient
import ru.forbion.f7cloud.core.push.F7PushEvent import ru.forbion.f7cloud.core.push.F7PushEvent
import ru.forbion.f7cloud.core.push.F7PushEventHub 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.F7BottomBarConfig
import ru.forbion.f7cloud.core.designsystem.F7MobileBottomBar import ru.forbion.f7cloud.core.designsystem.F7MobileBottomBar
import ru.forbion.f7cloud.core.designsystem.F7Colors 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.F7Theme
import ru.forbion.f7cloud.core.designsystem.f7SafeTopInsets import ru.forbion.f7cloud.core.designsystem.f7SafeTopInsets
import ru.forbion.f7cloud.mobile.OfficeEditorActivity import ru.forbion.f7cloud.mobile.OfficeEditorActivity
import ru.forbion.f7cloud.mobile.qr.F7QrScannerActivity import ru.forbion.f7cloud.mobile.qr.F7QrScannerActivity
import ru.forbion.f7cloud.mobile.OfficeWebViewPool
import ru.forbion.f7cloud.feature.files.OfficeWarmup import ru.forbion.f7cloud.feature.files.OfficeWarmup
import ru.forbion.f7cloud.mobile.permissions.F7AppPermissions import ru.forbion.f7cloud.mobile.permissions.F7AppPermissions
import ru.forbion.f7cloud.mobile.permissions.F7PermissionRationaleDialog import ru.forbion.f7cloud.mobile.permissions.F7PermissionRationaleDialog
@@ -95,8 +91,8 @@ fun AppScaffold(
onRequestRuntimePermissions: (onFinished: (() -> Unit)?) -> Unit = {}, onRequestRuntimePermissions: (onFinished: (() -> Unit)?) -> Unit = {},
) { ) {
val context = LocalContext.current val context = LocalContext.current
val authStore = remember { AuthStore(context) } val mainVm: MainViewModel = viewModel()
var session by remember { mutableStateOf(authStore.load()) } val session by mainVm.session.collectAsState()
var activeTab by rememberSaveable( var activeTab by rememberSaveable(
saver = Saver( saver = Saver(
save = { state -> state.value.name }, save = { state -> state.value.name },
@@ -151,17 +147,7 @@ fun AppScaffold(
context.applicationContext.getSharedPreferences("f7_permissions", android.content.Context.MODE_PRIVATE) context.applicationContext.getSharedPreferences("f7_permissions", android.content.Context.MODE_PRIVATE)
} }
val logoutScope = rememberCoroutineScope() val forceLogout = { mainVm.logout() }
val forceLogout = {
// Best-effort ревокация app password на сервере ДО очистки локальной сессии.
session?.let { current ->
logoutScope.launch { AppPasswordRevoker.revoke(current) }
}
OfficeWarmup.clear()
OfficeWebViewPool.dispose()
authStore.clear()
session = null
}
LaunchedEffect(session?.serverUrl, session?.username) { LaunchedEffect(session?.serverUrl, session?.username) {
session?.let { OfficeWarmup.warm(it) } session?.let { OfficeWarmup.warm(it) }
} }
@@ -174,10 +160,7 @@ fun AppScaffold(
F7Theme { F7Theme {
if (session == null) { if (session == null) {
LoginScreen(onLogin = { LoginScreen(onLogin = { mainVm.login(it) })
authStore.save(it)
session = it
})
return@F7Theme 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
}
}