Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

migrate LifecycleOwner into commonMain #433

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin-com
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
appCompat = { module = "androidx.appcompat:appcompat", version.ref = "appCompat" }
lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
lifecycle-savedState = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" }
lifecycle-viewModelKtx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
jetbrains-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
jetbrains-lifecycle-savedState = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" }
jetbrains-core-bundle = { module = "org.jetbrains.androidx.core:core-bundle", version = "1.0.0" }
compose-rxjava = { module = "androidx.compose.runtime:runtime-rxjava3", version.ref = "composeRuntime" }
compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "composeCompiler" }
compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "composeRuntime" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import cafe.adriel.voyager.core.registry.rememberScreen
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
Expand Down Expand Up @@ -43,7 +44,7 @@ class HomeScreen : Screen {
Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = { navigator.push(postListScreen) }
onClick = dropUnlessResumed { navigator.push(postListScreen) }
) {
Text(
text = "To Post List",
Expand All @@ -54,7 +55,7 @@ class HomeScreen : Screen {
Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = { navigator.push(postDetailsScreen) }
onClick = dropUnlessResumed { navigator.push(postDetailsScreen) }
) {
Text(
text = "To Post Details",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
Expand Down Expand Up @@ -44,7 +45,7 @@ data class DetailsScreen(
Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = { navigator.pop() }
onClick = dropUnlessResumed { navigator.pop() }
) {
Text(
text = "Return",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
Expand All @@ -35,7 +36,7 @@ class ListScreen : Screen {
Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = { navigator.pop() }
onClick = dropUnlessResumed { navigator.pop() }
) {
Text(
text = "Return",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import cafe.adriel.voyager.core.lifecycle.LifecycleEffect
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
Expand Down Expand Up @@ -62,7 +63,9 @@ public data class BasicNavigationScreen(
) {
Button(
enabled = navigator.canPop,
onClick = navigator::pop,
onClick = dropUnlessResumed {
navigator.pop()
},
modifier = Modifier.weight(.5f)
) {
Text(text = "Pop")
Expand All @@ -71,7 +74,9 @@ public data class BasicNavigationScreen(
Spacer(modifier = Modifier.weight(.1f))

Button(
onClick = { navigator.push(BasicNavigationScreen(index.inc(), wrapContent)) },
onClick = dropUnlessResumed {
navigator.push(BasicNavigationScreen(index.inc(), wrapContent))
},
modifier = Modifier.weight(.5f)
) {
Text(text = "Push")
Expand All @@ -80,7 +85,9 @@ public data class BasicNavigationScreen(
Spacer(modifier = Modifier.weight(.1f))

Button(
onClick = { navigator.replace(BasicNavigationScreen(index.inc(), wrapContent)) },
onClick = dropUnlessResumed {
navigator.replace(BasicNavigationScreen(index.inc(), wrapContent))
},
modifier = Modifier.weight(.5f)
) {
Text(text = "Replace")
Expand Down
5 changes: 3 additions & 2 deletions voyager-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ kotlin {
compileOnly(compose.runtime)
compileOnly(compose.runtimeSaveable)
implementation(libs.coroutines.core)
api(libs.jetbrains.lifecycle.runtime.compose)
api(libs.jetbrains.lifecycle.savedState)
api(libs.jetbrains.core.bundle)
}
jvmTest.dependencies {
implementation(libs.junit.api)
Expand All @@ -29,8 +32,6 @@ kotlin {
androidMain.dependencies {
implementation(libs.compose.activity)

implementation(libs.lifecycle.runtime)
implementation(libs.lifecycle.savedState)
implementation(libs.lifecycle.viewModelKtx)
implementation(libs.lifecycle.viewModelCompose)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,15 @@ package cafe.adriel.voyager.androidx

import android.app.Application
import android.content.Context
import android.os.Bundle
import android.content.ContextWrapper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.ProvidedValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.HasDefaultViewModelProviderFactory
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY
import androidx.lifecycle.SavedStateViewModelFactory
import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
Expand All @@ -31,34 +22,20 @@ import androidx.lifecycle.enableSavedStateHandles
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import cafe.adriel.voyager.core.lifecycle.CommonScreenLifecycleOwner
import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleOwner
import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleStore
import cafe.adriel.voyager.core.screen.Screen
import java.util.concurrent.atomic.AtomicReference

public class AndroidScreenLifecycleOwner private constructor() :
ScreenLifecycleOwner,
LifecycleOwner,
CommonScreenLifecycleOwner(),
ViewModelStoreOwner,
SavedStateRegistryOwner,
HasDefaultViewModelProviderFactory {

override val lifecycle: LifecycleRegistry = LifecycleRegistry(this)

override val viewModelStore: ViewModelStore = ViewModelStore()

private val atomicAppContext = AtomicReference<Context>()
internal val atomicParentLifecycleOwner = AtomicReference<LifecycleOwner>()

private val controller = SavedStateRegistryController.create(this)

private var isCreated: Boolean by mutableStateOf(false)

override val savedStateRegistry: SavedStateRegistry
get() = controller.savedStateRegistry

override val defaultViewModelProviderFactory: ViewModelProvider.Factory
get() = SavedStateViewModelFactory(
Expand All @@ -81,56 +58,25 @@ public class AndroidScreenLifecycleOwner private constructor() :
}

init {
controller.performAttach()
enableSavedStateHandles()
}

private fun onCreate(savedState: Bundle?) {
check(!isCreated) { "onCreate already called" }
isCreated = true
controller.performRestore(savedState)
initEvents.forEach {
lifecycle.safeHandleLifecycleEvent(it)
}
}

private fun emitOnStartEvents() {
startEvents.forEach {
lifecycle.safeHandleLifecycleEvent(it)
}
}

private fun emitOnStopEvents() {
stopEvents.forEach {
lifecycle.safeHandleLifecycleEvent(it)
}
}

@Composable
override fun ProvideBeforeScreenContent(
provideSaveableState: @Composable (suffixKey: String, content: @Composable () -> Unit) -> Unit,
content: @Composable () -> Unit
) {
provideSaveableState("lifecycle") {
LifecycleDisposableEffect()

val hooks = getHooks()

CompositionLocalProvider(*hooks.toTypedArray()) {
content()
}
}
}

override fun onDispose(screen: Screen) {
super.onDispose(screen)
viewModelStore.clear()
disposeEvents.forEach { event ->
lifecycle.safeHandleLifecycleEvent(event)
}
}

private fun performSave(outState: Bundle) {
controller.performSave(outState)
}

@Composable
Expand All @@ -140,103 +86,18 @@ public class AndroidScreenLifecycleOwner private constructor() :

return remember(this) {
listOf(
LocalLifecycleOwner provides this,
LocalViewModelStoreOwner provides this,
LocalSavedStateRegistryOwner provides this
)
}
}

/**
* Returns a unregister callback
*/
private fun registerLifecycleListener(outState: Bundle): () -> Unit {
val lifecycleOwner = atomicParentLifecycleOwner.get()
if (lifecycleOwner != null) {
val observer = object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
lifecycle.safeHandleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
}

override fun onResume(owner: LifecycleOwner) {
lifecycle.safeHandleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}

override fun onStart(owner: LifecycleOwner) {
lifecycle.safeHandleLifecycleEvent(Lifecycle.Event.ON_START)
}

override fun onStop(owner: LifecycleOwner) {
lifecycle.safeHandleLifecycleEvent(Lifecycle.Event.ON_STOP)

// when the Application goes to background, perform save
performSave(outState)
}
}
val lifecycle = lifecycleOwner.lifecycle
lifecycle.addObserver(observer)

return { lifecycle.removeObserver(observer) }
} else {
return { }
}
}

@Composable
private fun LifecycleDisposableEffect() {
val savedState = rememberSaveable { Bundle() }
if (!isCreated) {
onCreate(savedState) // do this in the UI thread to force it to be called before anything else
}

DisposableEffect(this) {
val unregisterLifecycle = registerLifecycleListener(savedState)
emitOnStartEvents()

onDispose {
unregisterLifecycle()

// when the screen goes to stack, perform save
performSave(savedState)

// notify lifecycle screen listeners
emitOnStopEvents()
}
}
}

private fun Context.getApplication(): Application? = when (this) {
private tailrec fun Context.getApplication(): Application? = when (this) {
is Application -> this
else -> null
}

private fun LifecycleRegistry.safeHandleLifecycleEvent(event: Lifecycle.Event) {
val currentState = currentState
if (!currentState.isAtLeast(Lifecycle.State.INITIALIZED)) return

handleLifecycleEvent(event)
}

public companion object {

private val initEvents = arrayOf(
Lifecycle.Event.ON_CREATE
)

private val startEvents = arrayOf(
Lifecycle.Event.ON_START,
Lifecycle.Event.ON_RESUME
)

private val stopEvents = arrayOf(
Lifecycle.Event.ON_PAUSE,
Lifecycle.Event.ON_STOP
)

private val disposeEvents = arrayOf(
Lifecycle.Event.ON_DESTROY
)

public fun get(screen: Screen): ScreenLifecycleOwner {
return ScreenLifecycleStore.get(screen) { AndroidScreenLifecycleOwner() }
}
Expand Down
Loading
Loading