Skip to content

Commit

Permalink
Merge branch 'v2.1' into v2.1-experimental-compose
Browse files Browse the repository at this point in the history
  • Loading branch information
arkivanov committed Jun 30, 2023
2 parents 0ce4e8c + 474637a commit 15266fb
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 0 deletions.
7 changes: 7 additions & 0 deletions decompose/api/android/decompose.api
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ public abstract interface annotation class com/arkivanov/decompose/ExperimentalD
public abstract interface annotation class com/arkivanov/decompose/InternalDecomposeApi : java/lang/annotation/Annotation {
}

public final class com/arkivanov/decompose/RetainedComponentKt {
public static final fun retainedComponent (Landroidx/activity/ComponentActivity;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static final fun retainedComponent (Landroidx/fragment/app/Fragment;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static synthetic fun retainedComponent$default (Landroidx/activity/ComponentActivity;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
public static synthetic fun retainedComponent$default (Landroidx/fragment/app/Fragment;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
}

public final class com/arkivanov/decompose/errorhandler/ErrorHandlersKt {
public static final fun getOnDecomposeError ()Lkotlin/jvm/functions/Function1;
public static final fun setOnDecomposeError (Lkotlin/jvm/functions/Function1;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.arkivanov.decompose

import androidx.activity.BackEventCompat
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.OnBackPressedDispatcher
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStoreOwner
import androidx.savedstate.SavedStateRegistryOwner
import com.arkivanov.essenty.backhandler.BackHandler
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.arkivanov.essenty.instancekeeper.instanceKeeper
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
import com.arkivanov.essenty.lifecycle.create
import com.arkivanov.essenty.lifecycle.destroy
import com.arkivanov.essenty.lifecycle.essentyLifecycle
import com.arkivanov.essenty.lifecycle.pause
import com.arkivanov.essenty.lifecycle.resume
import com.arkivanov.essenty.lifecycle.start
import com.arkivanov.essenty.lifecycle.stop
import com.arkivanov.essenty.lifecycle.subscribe
import com.arkivanov.essenty.parcelable.ParcelableContainer
import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher
import com.arkivanov.essenty.statekeeper.consume
import com.arkivanov.essenty.statekeeper.stateKeeper

/**
* Returns (creating if needed) a component that is retained over configuration changes.
* This is typically used to create a retained root component.
*
* Please pay attention when supplying dependencies to the component to avoid leaking the `Activity`.
*
* @param key a key of the component, must be unique within the `Activity`.
* @param handleBackButton a flag that determines whether back button handling is enabled or not, default is `true`.
* @param factory a function that returns a new instance of the component.
*/
@ExperimentalDecomposeApi
fun <T> ComponentActivity.retainedComponent(
key: String = "RootRetainedComponent",
handleBackButton: Boolean = true,
factory: (ComponentContext) -> T,
): T =
retainedComponent(
key = key,
onBackPressedDispatcher = if (handleBackButton) onBackPressedDispatcher else null,
isChangingConfigurations = ::isChangingConfigurations,
factory = factory,
)

/**
* Returns (creating if needed) a component that is retained over configuration changes.
* This is typically used to create a retained root component.
*
* Please pay attention when supplying dependencies to the component to avoid leaking the `Fragment`.
*
* @param key a key of the component, must be unique within the `Fragment`.
* @param handleBackButton a flag that determines whether back button handling is enabled or not, default is `true`.
* @param factory a function that returns a new instance of the component.
*/
@ExperimentalDecomposeApi
fun <T> Fragment.retainedComponent(
key: String = "RootRetainedComponent",
handleBackButton: Boolean = true,
factory: (ComponentContext) -> T,
): T =
retainedComponent(
key = key,
onBackPressedDispatcher = if (handleBackButton) requireActivity().onBackPressedDispatcher else null,
isChangingConfigurations = { activity?.isChangingConfigurations ?: false },
factory = factory,
)

private fun <T, O> O.retainedComponent(
key: String,
onBackPressedDispatcher: OnBackPressedDispatcher?,
isChangingConfigurations: () -> Boolean,
factory: (ComponentContext) -> T,
): T where O : LifecycleOwner, O : SavedStateRegistryOwner, O : ViewModelStoreOwner {
val lifecycle = essentyLifecycle()
val stateKeeper = stateKeeper()
val instanceKeeper = instanceKeeper()

check(!stateKeeper.isRegistered(key = key)) { "Another retained component is already registered with the key: $key" }

val holder =
instanceKeeper.getOrCreate(key = key) {
RetainedComponentHolder(
savedState = stateKeeper.consume(key = key),
factory = factory,
)
}

lifecycle.subscribe(
onCreate = { holder.lifecycle.create() },
onStart = { holder.lifecycle.start() },
onResume = { holder.lifecycle.resume() },
onPause = {
if (!isChangingConfigurations()) {
holder.lifecycle.pause()
}
},
onStop = {
if (!isChangingConfigurations()) {
holder.lifecycle.stop()
}
},
onDestroy = {
if (!isChangingConfigurations()) {
holder.lifecycle.destroy()
}
},
)

stateKeeper.register(key = key) { holder.stateKeeper.save() }

if (onBackPressedDispatcher != null) {
val onBackPressedCallback = DelegateOnBackPressedCallback(holder.onBackPressedDispatcher)
holder.onBackEnabledChangedListener = { onBackPressedCallback.isEnabled = it }
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}

return holder.component
}

private class DelegateOnBackPressedCallback(
private val dispatcher: OnBackPressedDispatcher,
) : OnBackPressedCallback(enabled = dispatcher.hasEnabledCallbacks()) {
override fun handleOnBackPressed() {
dispatcher.onBackPressed()
}

override fun handleOnBackStarted(backEvent: BackEventCompat) {
dispatcher.dispatchOnBackStarted(backEvent)
}

override fun handleOnBackProgressed(backEvent: BackEventCompat) {
dispatcher.dispatchOnBackProgressed(backEvent)
}

override fun handleOnBackCancelled() {
dispatcher.dispatchOnBackCancelled()
}
}

private class RetainedComponentHolder<out T>(
savedState: ParcelableContainer?,
factory: (ComponentContext) -> T,
) : InstanceKeeper.Instance {
val lifecycle: LifecycleRegistry = LifecycleRegistry()
val stateKeeper: StateKeeperDispatcher = StateKeeperDispatcher(savedState = savedState)
private val instanceKeeper: InstanceKeeperDispatcher = InstanceKeeperDispatcher()

var onBackEnabledChangedListener: ((Boolean) -> Unit)? = null

val onBackPressedDispatcher: OnBackPressedDispatcher =
OnBackPressedDispatcher(
fallbackOnBackPressed = null,
onHasEnabledCallbacksChanged = { onBackEnabledChangedListener?.invoke(it) },
)

val component: T =
factory(
DefaultComponentContext(
lifecycle = lifecycle,
stateKeeper = stateKeeper,
instanceKeeper = instanceKeeper,
backHandler = BackHandler(onBackPressedDispatcher),
)
)

override fun onDestroy() {
instanceKeeper.destroy()
}
}
20 changes: 20 additions & 0 deletions docs/component/instance-retaining.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,23 @@ class SomeComponent(
}
}
```

## Retained components

Although discouraged, it is still possible to have all components retained over configuration changes on Android. On the one hand, this makes `InstanceKeeper` no longer required. But on the other hand, this prevents from supplying dependencies that capture the hosting `Activity` or `Fragment`.

!!!warning
Pay attention when supplying dependencies to a retained component to avoid leaking the hosting `Activity` or `Fragment`.

```kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val root =
retainedComponent { componentContext ->
DefaultRootComponent(componentContext)
}
}
}
```

0 comments on commit 15266fb

Please sign in to comment.