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

Add BackHandler support on Android #1489

Merged
merged 1 commit into from
Sep 19, 2023
Merged
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
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 {
veyndan marked this conversation as resolved.
Show resolved Hide resolved
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
veyndan marked this conversation as resolved.
Show resolved Hide resolved
@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([email protected]) {
override fun handleOnBackPressed() {
[email protected]()
}
}
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