Skip to content

Commit

Permalink
Add BackHandler support on Android (#1493)
Browse files Browse the repository at this point in the history
  • Loading branch information
veyndan authored Sep 19, 2023
1 parent ae79819 commit 0716e02
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ 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 androidx.compose.runtime.staticCompositionLocalOf
import app.cash.redwood.ui.OnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher

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

Expand All @@ -37,6 +37,21 @@ public val OnBackPressedDispatcher.Companion.current: OnBackPressedDispatcher
@ReadOnlyComposable
get() = LocalOnBackPressedDispatcher.current

/**
* An effect for handling presses of the system back button.
*
* Calling this in your composable adds the given lambda to the [OnBackPressedDispatcher] of the
* [LocalOnBackPressedDispatcher].
*
* If this is called by nested composables, if enabled, the inner most composable will consume
* the call to system back and invoke its lambda. The call will continue to propagate up until it
* finds an enabled BackHandler.
*
* The [onBack] lambda is never invoked on platforms that don't have a system back button.
*
* @param enabled if this BackHandler should be enabled
* @param onBack the action invoked by pressing the system back
*/
// Multiplatform variant of
// https://github.com/androidx/androidx/blob/94ae1a9fb3ce778295e8cc724ae29f1231436bcb/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt#L82
@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,25 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [26])
class TreehouseLayoutTest {
private val context = RuntimeEnvironment.getApplication()!!
private val activity = Robolectric.buildActivity(ComponentActivity::class.java).resume().visible().get()

@Test fun widgetsAddChildViews() {
val activity = Robolectric.buildActivity(ComponentActivity::class.java).resume().visible().get()
val layout = TreehouseLayout(context, throwingWidgetSystem, activity.onBackPressedDispatcher)
val layout = TreehouseLayout(activity, throwingWidgetSystem, activity.onBackPressedDispatcher)

val view = View(context)
val view = View(activity)
layout.children.insert(0, viewWidget(view))
assertThat(layout.childCount).isEqualTo(1)
assertThat(layout.getChildAt(0)).isSameAs(view)
}

@Test fun attachAndDetachSendsStateChange() {
val activity = Robolectric.buildActivity(ComponentActivity::class.java).resume().visible().get()
val parent = activity.findViewById<ViewGroup>(android.R.id.content)
val layout = TreehouseLayout(context, throwingWidgetSystem, activity.onBackPressedDispatcher)
val layout = TreehouseLayout(activity, throwingWidgetSystem, activity.onBackPressedDispatcher)
val listener = CountingReadyForContentChangeListener<View>()

layout.readyForContentChangeListener = listener
Expand All @@ -76,46 +73,42 @@ class TreehouseLayoutTest {
}

@Test fun resetClearsUntrackedChildren() {
val activity = Robolectric.buildActivity(ComponentActivity::class.java).resume().visible().get()
val layout = TreehouseLayout(context, throwingWidgetSystem, activity.onBackPressedDispatcher)
val layout = TreehouseLayout(activity, throwingWidgetSystem, activity.onBackPressedDispatcher)

layout.addView(View(context))
layout.addView(View(activity))
assertThat(layout.childCount).isEqualTo(1)

layout.reset()
assertThat(layout.childCount).isEqualTo(0)
}

@Test fun resetClearsTrackedWidgets() {
val activity = Robolectric.buildActivity(ComponentActivity::class.java).resume().visible().get()
val layout = TreehouseLayout(context, throwingWidgetSystem, activity.onBackPressedDispatcher)
val layout = TreehouseLayout(activity, throwingWidgetSystem, activity.onBackPressedDispatcher)

// Needed to access internal state which cannot be reasonably observed through the public API.
val children = layout.children as ViewGroupChildren

children.insert(0, viewWidget(View(context)))
children.insert(0, viewWidget(View(activity)))
assertThat(children.widgets).hasSize(1)

layout.reset()
assertThat(children.widgets).hasSize(0)
}

@Test fun uiConfigurationReflectsInitialUiMode() {
val newConfig = Configuration(context.resources.configuration)
val newConfig = Configuration(activity.resources.configuration)
newConfig.uiMode = (newConfig.uiMode and UI_MODE_NIGHT_MASK.inv()) or UI_MODE_NIGHT_YES
val newContext = context.createConfigurationContext(newConfig) // Needs API 26.
val activity = Robolectric.buildActivity(ComponentActivity::class.java).resume().visible().get()
val newContext = activity.createConfigurationContext(newConfig) // Needs API 26.
val layout = TreehouseLayout(newContext, throwingWidgetSystem, activity.onBackPressedDispatcher)
assertThat(layout.uiConfiguration.value).isEqualTo(UiConfiguration(darkMode = true))
}

@Test fun uiConfigurationEmitsUiModeChanges() = runTest {
val activity = Robolectric.buildActivity(ComponentActivity::class.java).resume().visible().get()
val layout = TreehouseLayout(context, throwingWidgetSystem, activity.onBackPressedDispatcher)
val layout = TreehouseLayout(activity, throwingWidgetSystem, activity.onBackPressedDispatcher)
layout.uiConfiguration.test {
assertThat(awaitItem()).isEqualTo(UiConfiguration(darkMode = false))

val newConfig = Configuration(context.resources.configuration)
val newConfig = Configuration(activity.resources.configuration)
newConfig.uiMode = (newConfig.uiMode and UI_MODE_NIGHT_MASK.inv()) or UI_MODE_NIGHT_YES

layout.dispatchConfigurationChanged(newConfig)
Expand All @@ -124,16 +117,15 @@ class TreehouseLayoutTest {
}

@Test fun uiConfigurationEmitsSystemBarsSafeAreaInsetsChanges() = runTest {
val activity = Robolectric.buildActivity(ComponentActivity::class.java).resume().visible().get()
val layout = TreehouseLayout(context, throwingWidgetSystem, activity.onBackPressedDispatcher)
val layout = TreehouseLayout(activity, throwingWidgetSystem, activity.onBackPressedDispatcher)
layout.uiConfiguration.test {
assertThat(awaitItem()).isEqualTo(UiConfiguration(safeAreaInsets = Margin.Zero))
val insets = Insets.of(10, 20, 30, 40)
val windowInsets = WindowInsetsCompat.Builder()
.setInsets(WindowInsetsCompat.Type.systemBars(), insets)
.build()
ViewCompat.dispatchApplyWindowInsets(layout, windowInsets)
val expectedInsets = with(Density(context.resources)) {
val expectedInsets = with(Density(activity.resources)) {
Margin(
start = insets.left.toDp(),
end = insets.right.toDp(),
Expand Down

0 comments on commit 0716e02

Please sign in to comment.