Skip to content

Commit

Permalink
Add BackHandler support on Android (#1489)
Browse files Browse the repository at this point in the history
  • Loading branch information
veyndan authored Sep 19, 2023
1 parent 89d6b91 commit ff96a83
Show file tree
Hide file tree
Showing 42 changed files with 815 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.redwood.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import app.cash.redwood.ui.OnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher

public val LocalOnBackPressedDispatcher: ProvidableCompositionLocal<OnBackPressedDispatcher> =
compositionLocalOf {
throw AssertionError("OnBackPressedDispatcher was not provided!")
}

public val OnBackPressedDispatcher.Companion.current: OnBackPressedDispatcher
@Composable
@ReadOnlyComposable
get() = LocalOnBackPressedDispatcher.current

// Multiplatform variant of
// https://github.com/androidx/androidx/blob/94ae1a9fb3ce778295e8cc724ae29f1231436bcb/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt#L82
@Composable
public fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) {
// Safely update the current `onBack` lambda when a new one is provided.
val currentOnBack by rememberUpdatedState(onBack)
// Remember in Composition a back callback that calls the `onBack` lambda.
// Explicit return type necessary per https://youtrack.jetbrains.com/issue/KT-42073
val backCallback: OnBackPressedCallback = remember {
object : OnBackPressedCallback(enabled) {
override fun handleOnBackPressed() {
currentOnBack()
}
}
}
// On every successful composition, update the callback with the `enabled` value.
SideEffect {
backCallback.isEnabled = enabled
}
val backDispatcher = OnBackPressedDispatcher.current
DisposableEffect(backDispatcher) {
// Add callback to the backDispatcher.
val cancellable = backDispatcher.addCallback(backCallback)
// When the effect leaves the Composition, remove the callback.
onDispose {
cancellable.cancel()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshots.Snapshot
import app.cash.redwood.RedwoodCodegenApi
import app.cash.redwood.ui.OnBackPressedDispatcher
import app.cash.redwood.ui.UiConfiguration
import app.cash.redwood.widget.RedwoodView
import app.cash.redwood.widget.Widget
Expand All @@ -55,7 +56,14 @@ public fun <W : Any> RedwoodComposition(
): RedwoodComposition {
view.reset()

return RedwoodComposition(scope, view.children, view.uiConfiguration, provider, onEndChanges)
return RedwoodComposition(
scope,
view.children,
view.onBackPressedDispatcher,
view.uiConfiguration,
provider,
onEndChanges,
)
}

/**
Expand All @@ -65,15 +73,22 @@ public fun <W : Any> RedwoodComposition(
public fun <W : Any> RedwoodComposition(
scope: CoroutineScope,
container: Widget.Children<W>,
onBackPressedDispatcher: OnBackPressedDispatcher,
uiConfigurations: StateFlow<UiConfiguration>,
provider: Widget.Provider<W>,
onEndChanges: () -> Unit = {},
): RedwoodComposition {
return WidgetRedwoodComposition(scope, uiConfigurations, NodeApplier(provider, container, onEndChanges))
return WidgetRedwoodComposition(
scope,
onBackPressedDispatcher,
uiConfigurations,
NodeApplier(provider, container, onEndChanges),
)
}

private class WidgetRedwoodComposition<W : Any>(
private val scope: CoroutineScope,
private val onBackPressedDispatcher: OnBackPressedDispatcher,
private val uiConfigurations: StateFlow<UiConfiguration>,
applier: NodeApplier<W>,
) : RedwoodComposition {
Expand All @@ -100,7 +115,10 @@ private class WidgetRedwoodComposition<W : Any>(
override fun setContent(content: @Composable () -> Unit) {
composition.setContent {
val uiConfiguration by uiConfigurations.collectAsState()
CompositionLocalProvider(LocalUiConfiguration provides uiConfiguration) {
CompositionLocalProvider(
LocalOnBackPressedDispatcher provides onBackPressedDispatcher,
LocalUiConfiguration provides uiConfiguration,
) {
content()
}
}
Expand Down
5 changes: 5 additions & 0 deletions redwood-composeui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ kotlin {
implementation projects.redwoodWidgetCompose
}
}
androidMain {
dependencies {
implementation libs.androidx.activity.compose
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.redwood.composeui

import androidx.activity.OnBackPressedCallback as AndroidOnBackPressedCallback
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import app.cash.redwood.ui.Cancellable
import app.cash.redwood.ui.OnBackPressedCallback as RedwoodOnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher as RedwoodOnBackPressedDispatcher

@Composable
internal actual fun platformOnBackPressedDispatcher(): RedwoodOnBackPressedDispatcher {
val delegate = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
return remember(delegate) {
object : RedwoodOnBackPressedDispatcher {
override fun addCallback(onBackPressedCallback: RedwoodOnBackPressedCallback): Cancellable {
val androidOnBackPressedCallback = onBackPressedCallback.toAndroid()
delegate.addCallback(androidOnBackPressedCallback)
return object : Cancellable {
override fun cancel() {
androidOnBackPressedCallback.remove()
}
}
}
}
}
}

private fun RedwoodOnBackPressedCallback.toAndroid(): AndroidOnBackPressedCallback =
object : AndroidOnBackPressedCallback(this@toAndroid.isEnabled) {
override fun handleOnBackPressed() {
this@toAndroid.handleOnBackPressed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import app.cash.redwood.compose.RedwoodComposition
import app.cash.redwood.ui.Density
import app.cash.redwood.ui.OnBackPressedDispatcher
import app.cash.redwood.ui.Size
import app.cash.redwood.ui.UiConfiguration
import app.cash.redwood.ui.dp as redwoodDp
Expand All @@ -45,6 +46,8 @@ public fun RedwoodContent(
) {
val scope = rememberCoroutineScope()

val onBackPressedDispatcher = platformOnBackPressedDispatcher()

var viewportSize by remember { mutableStateOf(Size.Zero) }
val density = LocalDensity.current
val uiConfiguration = UiConfiguration(
Expand All @@ -57,6 +60,7 @@ public fun RedwoodContent(
val redwoodView = remember {
object : RedwoodView<@Composable () -> Unit> {
override val children = ComposeWidgetChildren()
override val onBackPressedDispatcher = onBackPressedDispatcher
override val uiConfiguration = MutableStateFlow(uiConfiguration)
override fun reset() {
children.remove(0, children.widgets.size)
Expand Down Expand Up @@ -85,3 +89,6 @@ public fun RedwoodContent(
redwoodView.children.render()
}
}

@Composable
internal expect fun platformOnBackPressedDispatcher(): OnBackPressedDispatcher
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.redwood.composeui

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import app.cash.redwood.ui.Cancellable
import app.cash.redwood.ui.OnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher

@Composable
internal actual fun platformOnBackPressedDispatcher(): OnBackPressedDispatcher {
return remember {
object : OnBackPressedDispatcher {
override fun addCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable =
object : Cancellable {
override fun cancel() = Unit
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.redwood.composeui

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import app.cash.redwood.ui.Cancellable
import app.cash.redwood.ui.OnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher

@Composable
internal actual fun platformOnBackPressedDispatcher(): OnBackPressedDispatcher {
return remember {
object : OnBackPressedDispatcher {
override fun addCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable =
object : Cancellable {
override fun cancel() = Unit
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.redwood.composeui

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import app.cash.redwood.ui.Cancellable
import app.cash.redwood.ui.OnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher

@Composable
internal actual fun platformOnBackPressedDispatcher(): OnBackPressedDispatcher {
return remember {
object : OnBackPressedDispatcher {
override fun addCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable =
object : Cancellable {
override fun cancel() = Unit
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.runtime.MonotonicFrameClock
import app.cash.redwood.compose.LocalWidgetVersion
import app.cash.redwood.compose.RedwoodComposition
import app.cash.redwood.protocol.ChangesSink
import app.cash.redwood.ui.OnBackPressedDispatcher
import app.cash.redwood.ui.UiConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -34,9 +35,10 @@ public fun ProtocolRedwoodComposition(
bridge: ProtocolBridge,
changesSink: ChangesSink,
widgetVersion: UInt,
onBackPressedDispatcher: OnBackPressedDispatcher,
uiConfigurations: StateFlow<UiConfiguration>,
): RedwoodComposition {
val composition = RedwoodComposition(scope, bridge.root, uiConfigurations, bridge.provider) {
val composition = RedwoodComposition(scope, bridge.root, onBackPressedDispatcher, uiConfigurations, bridge.provider) {
bridge.getChangesOrNull()?.let(changesSink::sendChanges)
}
return ProtocolRedwoodComposition(composition, widgetVersion)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import app.cash.redwood.protocol.PropertyChange
import app.cash.redwood.protocol.PropertyTag
import app.cash.redwood.protocol.WidgetTag
import app.cash.redwood.testing.TestRedwoodComposition
import app.cash.redwood.ui.Cancellable
import app.cash.redwood.ui.OnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher
import app.cash.redwood.ui.UiConfiguration
import assertk.assertThat
import assertk.assertions.isEqualTo
Expand All @@ -58,6 +61,13 @@ class ProtocolTest {
bridge = bridge,
changesSink = ::error,
widgetVersion = 22U,
onBackPressedDispatcher = object : OnBackPressedDispatcher {
override fun addCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable {
return object : Cancellable {
override fun cancel() = Unit
}
}
},
uiConfigurations = MutableStateFlow(UiConfiguration()),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.redwood.ui

public interface Cancellable {
public fun cancel()
}
Loading

0 comments on commit ff96a83

Please sign in to comment.