Last active
December 9, 2022 00:31
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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