diff --git a/redwood-compose/src/commonMain/kotlin/app/cash/redwood/compose/BackHandler.kt b/redwood-compose/src/commonMain/kotlin/app/cash/redwood/compose/BackHandler.kt index 10104566eb..d2ae23e4f5 100644 --- a/redwood-compose/src/commonMain/kotlin/app/cash/redwood/compose/BackHandler.kt +++ b/redwood-compose/src/commonMain/kotlin/app/cash/redwood/compose/BackHandler.kt @@ -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 = - compositionLocalOf { + staticCompositionLocalOf { throw AssertionError("OnBackPressedDispatcher was not provided!") } @@ -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 diff --git a/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt b/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt index 20b36ee8c7..32ce85112c 100644 --- a/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt +++ b/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt @@ -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(android.R.id.content) - val layout = TreehouseLayout(context, throwingWidgetSystem, activity.onBackPressedDispatcher) + val layout = TreehouseLayout(activity, throwingWidgetSystem, activity.onBackPressedDispatcher) val listener = CountingReadyForContentChangeListener() layout.readyForContentChangeListener = listener @@ -76,10 +73,9 @@ 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() @@ -87,13 +83,12 @@ class TreehouseLayoutTest { } @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() @@ -101,21 +96,19 @@ class TreehouseLayoutTest { } @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) @@ -124,8 +117,7 @@ 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) @@ -133,7 +125,7 @@ class TreehouseLayoutTest { .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(),