enum class StartupPhase(val order: Int) {
// The app cannot function if this fails (e.g., Security Configs).
CRITICAL(0),
// Crucial for the first screen (e.g., Feature Flags).
HIGH(1),
// Standard startup tasks (e.g., Pre-warming caches).
NORMAL(2),
// Fire-and-forget, or things that can wait (e.g., Background Observers).
LOW(3)
}
// Sealed to prevent rogue custom lifecycles
sealed interface StartupTask {
val phase: StartupPhase get() = StartupPhase.NORMAL
// Tip: Always profile this timeout on low-end hardware under heavy CPU load!
val timeoutMs: Long get() = 800L
suspend operator fun invoke()
}
interface AppStartupTask : StartupTask
interface ForegroundTask : StartupTask
interface SessionStartupTask : StartupTask
// Thread-safe base class for tasks that SHOULD block and retry safely
abstract class IdempotentStartupTask : StartupTask {
private val mutex = Mutex()
@Volatile private var isCompleted = false
final override suspend operator fun invoke() {
if (isCompleted) return
mutex.withLock {
if (isCompleted) return@withLock
execute()
isCompleted = true
}
}
protected abstract suspend fun execute()
}
// A blocking task (delays the next phase until finished)
class SecurityConfigTask @Inject constructor(
private val securityManager: SecurityManager
) : IdempotentStartupTask(), AppStartupTask {
override val phase = StartupPhase.CRITICAL
override suspend fun execute() {
securityManager.initializeEncryptionKeys()
}
}
// A continuous task (fire-and-forget observer)
class PendingSyncObserverTask @Inject constructor(
private val offlineSyncDao: OfflineSyncDao,
private val syncManager: SyncManager,
@ApplicationScope private val appScope: CoroutineScope
) : AppStartupTask {
override val phase = StartupPhase.LOW
override suspend fun invoke() {
appScope.launch {
offlineSyncDao.observePendingActions().collect { pendingItems ->
if (pendingItems.isNotEmpty()) {
syncManager.startUploadProcess(pendingItems)
}
}
}
}
}
// A decentralized, dynamic consent-gated task (GDPR, CCPA, CPRA)
class AnalyticsConsentTask @Inject constructor(
private val consentManager: ConsentManager,
@ApplicationScope private val appScope: CoroutineScope
) : AppStartupTask {
override val phase = StartupPhase.LOW
override suspend fun invoke() {
appScope.launch {
// Continuously observes this specific vendor's consent state.
// Handles cold start, delayed CMP acceptance, AND later revocation in settings!
// Multiple tasks doing this takes virtually zero resources as suspended flows are cheap.
consentManager.observeConsent(Vendor.FIREBASE_ANALYTICS).collect { isGranted ->
Analytics.setCollectionEnabled(isGranted)
}
}
}
}
12345678
// Example: How feature modules contribute tasks to the global Set in Hilt/Dagger
@Module
@InstallIn(SingletonComponent::class)
abstract class SecurityModule {
@Binds
@IntoSet
abstract fun bindSecurityConfigTask(task: SecurityConfigTask): AppStartupTask
}
class RemoteConfigTask @Inject constructor(
private val configManager: ConfigManager
) : IdempotentStartupTask(), ForegroundTask {
override val phase = StartupPhase.HIGH
override val timeoutMs = 5000L // Giving the network a bit more time if needed
override suspend fun execute() {
configManager.updateRemoteConfigs()
}
}
123456789101112131415
@Singleton
class AppLifecycleObserver @Inject constructor(
private val foregroundTasks: Set<@JvmSuppressWildcards ForegroundTask>,
@ApplicationScope private val scope: CoroutineScope
) : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
scope.launch {
try {
foregroundTasks.executeAll()
} catch (e: CriticalStartupException) {
// Log non-fatal foreground failure and do not crash the app
}
}
}
}
@HiltViewModel
class HomeViewModel @Inject constructor(
private val fetchFeedUseCase: FetchFeedUseCase
) : ViewModel() {
// No init blocks. No LaunchedEffect. No SideEffect.
// Data fetching starts automatically when the UI subscribes.
val uiState: StateFlow<HomeState> = flow {
// Wrap a one-shot suspend call into a cold flow
emit(fetchFeedUseCase())
}.map {
// Map result to UI state (e.g. Success, Error) here
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = HomeState.Loading
)
}
1234567
@Composable
fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
// Lifecycle-aware collection stops wasting resources when the app is backgrounded
val state by viewModel.uiState.collectAsStateWithLifecycle()
// Render UI...
}