Skip to content

Instantly share code, notes, and snippets.

@kmerrell42
Last active December 9, 2022 00:31
Show Gist options
  • Save kmerrell42/e99aa6ec83024b7fc05bea7ad77f659a to your computer and use it in GitHub Desktop.
Save kmerrell42/e99aa6ec83024b7fc05bea7ad77f659a to your computer and use it in GitHub Desktop.
Sample implementation and extension function to help with handling initialization behind the Android 12+ “system” Splash Screen.
package io.mercury.example
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.mercury.altitude.LaunchActivity.LaunchViewModelAsInitializerDelegatesToFeature.EngineFeature.EngineState
import io.mercury.android.splashscreen.Initializer
import io.mercury.android.splashscreen.SimpleInitializer
import io.mercury.android.splashscreen.initializeBehindSplashScreen
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/**
* A sample Activity using [Activity.initializeBehindSplashScreen] to initialize the app behind the
* Android @see [Splash Screen](https://developer.android.com/develop/ui/views/launch/splash-screen) API.
*/
class LaunchActivity : ComponentActivity() {
private val vm by viewModels<LaunchViewModelSimple>()
// private val vm by viewModels<LaunchViewModelAsInitializer>()
// private val vm by viewModels<LaunchViewModelAsInitializerDelegatesToFeature>()
override fun onCreate(savedInstanceState: Bundle?) {
val initializer = vm.initializer
// val initializer = vm
initializeBehindSplashScreen(initializer)
super.onCreate(savedInstanceState)
setContent {
Surface {
// NOTE: This is rendered UNDER the splash screen, not synchronously AFTER it is
// dismissed. If you want to do something in response to the splash screen being
// dismissed, provide the onSplashRemoved callback to initializeBehindSplashScreen.
Text(text = "The LaunchActivity shown to the user")
}
}
}
class LaunchViewModelSimple : ViewModel() {
val initializer = SimpleInitializer(viewModelScope) { onComplete ->
// This is where your long-running initialization code would go.
delay(5000)
onComplete() // Must be called to dismiss the splash screen.
}
}
class LaunchViewModelAsInitializer : ViewModel(), Initializer {
private val statePublisher = MutableStateFlow<LaunchState>(LaunchState.Unloaded)
val state = statePublisher.asStateFlow()
override fun initialize() {
viewModelScope.launch {
statePublisher.value = LaunchState.Loading
// This is where your long-running initialization code would go.
delay(5000)
statePublisher.value = LaunchState.Loaded("I'm loaded!")
}
}
override fun isInitialized(): Boolean {
return state.value is LaunchState.Loaded || state.value is LaunchState.ErrorLoading
}
interface LaunchState {
object Unloaded : LaunchState
object Loading : LaunchState
data class Loaded(val string: String) : LaunchState
data class ErrorLoading(val error: Throwable) : LaunchState
}
}
class LaunchViewModelAsInitializerDelegatesToFeature : ViewModel(), Initializer {
val feature = EngineFeature(viewModelScope)
override fun initialize() {
feature.startEngine()
}
override fun isInitialized(): Boolean {
return feature.state.value is EngineState.Started || feature.state.value is EngineState.EngineFailure
}
class EngineFeature(private val scope: CoroutineScope) {
private val statePublisher = MutableStateFlow<EngineState>(EngineState.NotRunning)
val state = statePublisher.asStateFlow()
fun startEngine() {
scope.launch {
statePublisher.value = EngineState.Starting
// This is where your long-running initialization code would go.
delay(5000)
statePublisher.value = EngineState.Started("I'm loaded!")
}
}
interface EngineState {
object NotRunning : EngineState
object Starting : EngineState
data class Started(val string: String) : EngineState
data class EngineFailure(val error: Throwable) : EngineState
}
}
}
}
package io.mercury.android.splashscreen
import android.app.Activity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import io.mercury.android.splashscreen.SimpleInitializer.State.Initialized
import io.mercury.android.splashscreen.SimpleInitializer.State.Initializing
import io.mercury.android.splashscreen.SimpleInitializer.State.Uninitialized
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
/**
* This function will install a splash screen and then run the [initializer] before hiding the
* splash screen.
*
* This builds upon the Compat version of the SplashScreen API, and your app will need to be
* configured accordingly to customize that look/behavior. Best to get things working with that
* first, before replacing the call to [Activity.installSplashScreen] with this function.
* https://developer.android.com/reference/kotlin/androidx/core/splashscreen/SplashScreen
*
* @param initializer - The [Initializer] containing the initialization code that will be ran. To
* handle initialization between Activity recreations (on rotation), the caller is responsible for
* either cancelling the previous initialization, or ensuring that the [Initializer] lives through
* recreation. The simplest approach is to house it in a ViewModel tied to the activity.
* @param onSplashRemoved - Optional callback that will be called when the splash screen is being
* removed.
*/
fun Activity.initializeBehindSplashScreen(
initializer: Initializer,
onSplashRemoved: () -> Unit = {}
) {
initializer.initialize()
installSplashScreen().apply {
// Keep the SplashScreen up until we have initialized the feature/app
setKeepOnScreenCondition { !initializer.isInitialized() }
setOnExitAnimationListener { splashScreenViewProvider ->
splashScreenViewProvider.remove()
onSplashRemoved()
}
}
}
interface Initializer {
fun initialize()
fun isInitialized(): Boolean
}
class SimpleInitializer constructor(
private val scope: CoroutineScope,
private val initializeBlock: suspend (onCompleteCallback: () -> Unit) -> Unit
) : Initializer {
private var state = MutableStateFlow<State>(Uninitialized)
override fun initialize() {
if (state.value == Uninitialized) {
state.value = Initializing
scope.launch {
initializeBlock { state.value = Initialized }
}
}
}
override fun isInitialized() = state.value == Initialized
sealed interface State {
object Uninitialized : State
object Initializing : State
object Initialized : State
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment