From e423bd6c581cee2172039ed79d45fd7d9985c3bc Mon Sep 17 00:00:00 2001 From: OneWay Date: Sun, 4 Oct 2026 12:15:00 +0000 Subject: [PATCH] refactor(main): expose cold-start preferences as an InitialLaunch flow Read the theme, locale, empty-library seed and section expansion once in the application and collect them as state, instead of blocking the UI thread with synchronous reads before setContent. --- .../java/com/example/research/MainActivity.kt | 84 ++----------------- .../example/research/ReSearchApplication.kt | 40 +++++++++ 2 files changed, 47 insertions(+), 77 deletions(-) diff --git a/app/src/main/java/com/example/research/MainActivity.kt b/app/src/main/java/com/example/research/MainActivity.kt index eaeb56d..985f7ce 100644 --- a/app/src/main/java/com/example/research/MainActivity.kt +++ b/app/src/main/java/com/example/research/MainActivity.kt @@ -31,8 +31,6 @@ import com.example.research.feature.search.SearchAction import com.example.research.feature.search.SearchViewModel import com.example.research.ui.navigation.AppNavigation import com.example.research.ui.settings.SettingsViewModel -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking class MainActivity : ComponentActivity() { companion object { @@ -52,8 +50,6 @@ class MainActivity : ComponentActivity() { } } - private var initialSectionExpansion: SectionExpansionStates? = null - private val settingsViewModel: SettingsViewModel by viewModels { val app = application as ReSearchApplication val versionName = try { @@ -72,7 +68,7 @@ class MainActivity : ComponentActivity() { app.dictionaryImportManager, app.dictionaryProgressStateHolder, versionName, - initialSectionExpansion ?: SectionExpansionStates( + app.initialLaunch.value?.sectionExpansion ?: SectionExpansionStates( isThemeExpanded = false, isLanguageExpanded = false, isDictionariesExpanded = true, @@ -87,19 +83,16 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) requestNotificationPermissionIfNeeded() - // Load theme and language synchronously before setContent to prevent flashing val app = application as ReSearchApplication - val initialTheme = loadInitialTheme(app) - val initialLanguage = loadInitialLanguage(app) - val initialHadNoDictionaries = loadInitialHadNoDictionaries(app) - initialSectionExpansion = loadInitialSectionExpansion(app) - handleIntent(intent) setContent { + val initialLaunch by app.initialLaunch.collectAsStateWithLifecycle() + val launch = initialLaunch ?: return@setContent + val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle() // Use loaded values initially, then switch to state values when ready - val theme = settingsState.theme.takeIf { it != AppTheme.SYSTEM } ?: initialTheme - val language = settingsState.language.takeIf { it != "system" } ?: initialLanguage + val theme = settingsState.theme.takeIf { it != AppTheme.SYSTEM } ?: launch.theme + val language = settingsState.language.takeIf { it != "system" } ?: launch.language val darkTheme = when (theme) { AppTheme.LIGHT -> false AppTheme.DARK -> true @@ -115,7 +108,7 @@ class MainActivity : ComponentActivity() { AppNavigation( searchViewModel = searchViewModel, settingsViewModel = settingsViewModel, - seedNoDictionaries = initialHadNoDictionaries + seedNoDictionaries = launch.seedNoDictionaries ) } } @@ -138,69 +131,6 @@ class MainActivity : ComponentActivity() { } } - /** - * Load initial theme synchronously from DataStore to prevent theme flashing. - * This is called before setContent to ensure the correct theme is applied immediately. - * Uses runBlocking to synchronously read from DataStore. - */ - private fun loadInitialTheme(app: ReSearchApplication): AppTheme { - return try { - runBlocking { - val themeString = app.preferencesManager.themeMode.first() - try { - AppTheme.valueOf(themeString) - } catch (_: Exception) { - android.util.Log.w("MainActivity", "Invalid theme value: $themeString, using SYSTEM") - AppTheme.SYSTEM - } - } - } catch (e: Exception) { - android.util.Log.e("MainActivity", "Failed to load theme preference: ${e.message}") - AppTheme.SYSTEM - } - } - - /** - * Load initial language synchronously from DataStore to prevent language flashing. - * This is called before setContent to ensure the correct language is applied immediately. - * Uses runBlocking to synchronously read from DataStore. - */ - private fun loadInitialLanguage(app: ReSearchApplication): String { - return try { - runBlocking { - app.preferencesManager.language.first() - } - } catch (e: Exception) { - android.util.Log.e("MainActivity", "Failed to load language preference: ${e.message}") - "system" - } - } - - private fun loadInitialHadNoDictionaries(app: ReSearchApplication): Boolean { - return try { - runBlocking { - app.preferencesManager.hadNoDictionaries.first() - } - } catch (_: Exception) { - true - } - } - - private fun loadInitialSectionExpansion(app: ReSearchApplication): SectionExpansionStates { - return try { - runBlocking { - app.preferencesManager.sectionExpansionStates() - } - } catch (e: Exception) { - android.util.Log.e("MainActivity", "Failed to load section expansion states: ${e.message}") - SectionExpansionStates( - isThemeExpanded = false, - isLanguageExpanded = false, - isDictionariesExpanded = true, - ) - } - } - override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handleIntent(intent) diff --git a/app/src/main/java/com/example/research/ReSearchApplication.kt b/app/src/main/java/com/example/research/ReSearchApplication.kt index e66771e..7d216db 100644 --- a/app/src/main/java/com/example/research/ReSearchApplication.kt +++ b/app/src/main/java/com/example/research/ReSearchApplication.kt @@ -3,7 +3,9 @@ package com.example.research import android.app.Application import com.example.research.common.progress.DictionaryProgressStateHolder import com.example.research.common.util.NotificationHelper +import com.example.research.core.domain.model.AppTheme import com.example.research.data.local.preferences.PreferencesManager +import com.example.research.data.local.preferences.SectionExpansionStates import com.example.research.data.repository.LocalDictionaryRepository import com.example.research.feature.download.DownloadManager import com.example.research.feature.download.repository.DictionaryRepository @@ -12,10 +14,25 @@ import com.example.research.feature.import.DictionaryImportManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import okhttp3.OkHttpClient import okhttp3.Protocol +/** + * Values a cold start needs before the first composed frame: the initial + * theme, locale, empty-library navigation seed, and settings expansion state. + */ +data class InitialLaunch( + val theme: AppTheme, + val language: String, + val seedNoDictionaries: Boolean, + val sectionExpansion: SectionExpansionStates, +) + class ReSearchApplication : Application() { lateinit var preferencesManager: PreferencesManager @@ -43,6 +60,9 @@ class ReSearchApplication : Application() { private set private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val _initialLaunch = MutableStateFlow(null) + val initialLaunch: StateFlow = _initialLaunch.asStateFlow() + override fun onCreate() { super.onCreate() @@ -53,6 +73,9 @@ class ReSearchApplication : Application() { applicationScope.launch(Dispatchers.IO) { preferencesManager.sanitizeDictionarySources() } + applicationScope.launch(Dispatchers.IO) { + _initialLaunch.value = loadInitialLaunch() + } localDictionaryRepository = LocalDictionaryRepository(this, preferencesManager = preferencesManager) okHttpClient = OkHttpClient.Builder() @@ -121,6 +144,23 @@ class ReSearchApplication : Application() { downloadManager.onTerminal = dictionaryPipelineCoordinator::onDownloadTerminal } + private suspend fun loadInitialLaunch(): InitialLaunch { + val defaultExpansions = SectionExpansionStates( + isThemeExpanded = false, + isLanguageExpanded = false, + isDictionariesExpanded = true, + ) + return InitialLaunch( + theme = runCatching { AppTheme.valueOf(preferencesManager.themeMode.first()) } + .getOrDefault(AppTheme.SYSTEM), + language = runCatching { preferencesManager.language.first() }.getOrDefault("system"), + seedNoDictionaries = runCatching { preferencesManager.hadNoDictionaries.first() } + .getOrDefault(true), + sectionExpansion = runCatching { preferencesManager.sectionExpansionStates() } + .getOrDefault(defaultExpansions), + ) + } + override fun onTrimMemory(level: Int) { super.onTrimMemory(level) if (::localDictionaryRepository.isInitialized) {