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.
This commit is contained in:
1 parent
0bae625c01
commit
e423bd6c58
2 files changed
+47
-77
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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<InitialLaunch?>(null)
|
||||
val initialLaunch: StateFlow<InitialLaunch?> = _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) {
|
||||
|
||||
Reference in new issue
Block a user