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

Container and Wrapper interfaces, improved forWrapper() #920

Merged
merged 8 commits into from
Mar 16, 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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ subprojects {
if (file("src/${dokkaSourceSet.name}").exists()) {

val readmeFile = file("$projectDir/README.md")
// If the module has a README, add it to the the module's index
// If the module has a README, add it to the module's index
if (readmeFile.exists()) {
includes.from(readmeFile)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import com.squareup.workflow1.ui.container.ModalOverlay
import com.squareup.workflow1.ui.container.ScreenOverlay

@OptIn(WorkflowUiExperimentalApi::class)
class PanelOverlay<T : Screen>(
override val content: T
) : ScreenOverlay<T>, ModalOverlay
class PanelOverlay<C : Screen>(
override val content: C
) : ScreenOverlay<C>, ModalOverlay {
override fun <D : Screen> map(transform: (C) -> D): PanelOverlay<D> =
PanelOverlay(transform(content))
}
25 changes: 25 additions & 0 deletions samples/nested-overlays/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id("com.android.application")
`kotlin-android`
`android-sample-app`
`android-ui-tests`
}

android {
defaultConfig {
applicationId = "com.squareup.sample.nestedoverlays"
}
namespace = "com.squareup.sample.nestedoverlays"
}

dependencies {
debugImplementation(libs.squareup.leakcanary.android)

implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.viewbinding)

implementation(project(":workflow-ui:core-android"))
implementation(project(":workflow-ui:core-common"))
}
4 changes: 4 additions & 0 deletions samples/nested-overlays/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 7.4.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.4.1)" variant="fatal" version="7.4.1">

</issues>
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.squareup.sample.nestedoverlays

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withParentIndex
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule
import leakcanary.DetectLeaksAfterTestSuccess
import org.hamcrest.core.AllOf.allOf
import org.hamcrest.core.IsNot.not
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class NestedOverlaysAppTest {

private val scenarioRule = ActivityScenarioRule(NestedOverlaysActivity::class.java)

@get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
.around(scenarioRule)
.around(IdlingDispatcherRule)

@Test fun basics() {
onTopCoverBody().assertDisplayed()
onTopCoverEverything().assertDisplayed()
onBottomCoverBody().assertDisplayed()
onBottomCoverEverything().assertDisplayed()

onTopCoverBody().perform(click())
onView(withText("Close")).perform(click())
onTopCoverEverything().perform(click())
onView(withText("Close")).perform(click())

onView(withText("Hide Top Bar")).perform(click())
onTopCoverBody().assertNotDisplayed()
onTopCoverEverything().assertNotDisplayed()
onBottomCoverBody().assertDisplayed()
onBottomCoverEverything().assertDisplayed()

onView(withText("Hide Bottom Bar")).perform(click())
onTopCoverBody().assertNotDisplayed()
onTopCoverEverything().assertNotDisplayed()
onBottomCoverBody().assertNotDisplayed()
onBottomCoverEverything().assertNotDisplayed()
}

// https://github.com/square/workflow-kotlin/issues/966
@Test fun canInsertDialog() {
onTopCoverEverything().perform(click())
onView(withText("Hide Top Bar")).check(doesNotExist())
onView(withText("Cover Body")).perform(click())

// This line fails due to https://github.com/square/workflow-kotlin/issues/966
// onView(withText("Hide Top Bar")).check(doesNotExist())

// Should continue to close the top sheet and assert that the inner sheet is visible.
}

// So far can't express this in Espresso. Considering move to Maestro
// @Test fun canClickPastInnerWindow() {
// onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0))))
// .perform(click())
//
// scenario.onActivity { activity ->
// onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0))))
// .inRoot(withDecorView(not(`is`(activity.window.decorView))))
// .perform(click())
// }
// }

private fun ViewInteraction.assertNotDisplayed() {
check(matches(not(isDisplayed())))
}

private fun ViewInteraction.assertDisplayed() {
check(matches(isDisplayed()))
}

private fun onBottomCoverEverything() =
onView(allOf(withText("Cover Everything"), withParent(withParentIndex(2))))

private fun onBottomCoverBody() =
onView(allOf(withText("Cover Body"), withParent(withParentIndex(2))))

private fun onTopCoverBody() =
onView(allOf(withText("Cover Body"), withParent(withParentIndex(0))))

private fun onTopCoverEverything() =
onView(allOf(withText("Cover Everything"), withParent(withParentIndex(0))))
}
23 changes: 23 additions & 0 deletions samples/nested-overlays/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:allowBackup="false"
android:label="@string/app_name"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning,MissingApplicationIcon"
>

<activity android:name="com.squareup.sample.nestedoverlays.NestedOverlaysActivity"
android:exported="true">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

</activity>

</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.squareup.sample.nestedoverlays

import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.widget.LinearLayout
import androidx.annotation.ColorRes
import androidx.annotation.StringRes
import androidx.core.view.get
import com.squareup.workflow1.ui.AndroidScreen
import com.squareup.workflow1.ui.ScreenViewFactory
import com.squareup.workflow1.ui.ScreenViewHolder
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import android.widget.Button as ButtonView

data class Button(
@StringRes val name: Int,
val onClick: () -> Unit
)

@OptIn(WorkflowUiExperimentalApi::class)
class ButtonBar(
vararg buttons: Button?,
@ColorRes val color: Int = -1,
) : AndroidScreen<ButtonBar> {
private val buttons: List<Button> = buttons.filterNotNull().toList()

override val viewFactory =
ScreenViewFactory.fromCode<ButtonBar> { _, initialEnvironment, context, _ ->
LinearLayout(context).let { view ->
@Suppress("DEPRECATION")
if (color > -1) view.background = ColorDrawable(view.resources.getColor(color))

view.gravity = Gravity.CENTER

ScreenViewHolder(initialEnvironment, view) { bar, _ ->
val existing = view.childCount

bar.buttons.forEachIndexed { index, button ->
val buttonView = if (index < existing) {
view[index] as ButtonView
} else {
ButtonView(context).also { view.addView(it) }
}
with(buttonView) {
text = view.resources.getText(button.name)
setOnClickListener { button.onClick() }
}
}
for (i in bar.buttons.size until view.childCount) view.removeViewAt(i)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@file:OptIn(WorkflowExperimentalRuntime::class)

package com.squareup.sample.nestedoverlays

import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.WorkflowLayout
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.renderWorkflowIn
import kotlinx.coroutines.flow.StateFlow

@OptIn(WorkflowUiExperimentalApi::class)
class NestedOverlaysActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// This ViewModel will survive configuration changes. It's instantiated
// by the first call to viewModels(), and that original instance is returned by
// succeeding calls.
val model: NestedOverlaysViewModel by viewModels()
setContentView(
WorkflowLayout(this).apply { take(lifecycle, model.renderings) }
)
}
}

class NestedOverlaysViewModel(savedState: SavedStateHandle) : ViewModel() {
@OptIn(WorkflowUiExperimentalApi::class)
val renderings: StateFlow<Screen> by lazy {
renderWorkflowIn(
workflow = NestedOverlaysWorkflow,
scope = viewModelScope,
savedStateHandle = savedState,
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
@file:OptIn(WorkflowUiExperimentalApi::class)

package com.squareup.sample.nestedoverlays

import com.squareup.sample.nestedoverlays.NestedOverlaysWorkflow.State
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen
import com.squareup.workflow1.ui.container.FullScreenOverlay
import com.squareup.workflow1.ui.container.Overlay

typealias NestedOverlaysRendering = BodyAndOverlaysScreen<
TopAndBottomBarsScreen<BodyAndOverlaysScreen<Screen, Overlay>>,
Overlay
>

object NestedOverlaysWorkflow : StatefulWorkflow<Unit, State, Nothing, NestedOverlaysRendering>() {
data class State(
val showTopBar: Boolean = true,
val showBottomBar: Boolean = true,
val showInnerSheet: Boolean = false,
val showOuterSheet: Boolean = false
)

override fun initialState(
props: Unit,
snapshot: Snapshot?
) = State()

override fun render(
renderProps: Unit,
renderState: State,
context: RenderContext
): NestedOverlaysRendering {
val toggleTopBarButton = Button(
name = if (renderState.showTopBar) R.string.HIDE_TOP else R.string.SHOW_TOP,
onClick = context.eventHandler { state = state.copy(showTopBar = !state.showTopBar) }
)

val toggleBottomBarButton = Button(
name = if (renderState.showBottomBar) R.string.HIDE_BOTTOM else R.string.SHOW_BOTTOM,
onClick = context.eventHandler { state = state.copy(showBottomBar = !state.showBottomBar) }
)

val outerSheet = if (!renderState.showOuterSheet) {
null
} else {
FullScreenOverlay(
ButtonBar(
Button(
name = R.string.CLOSE,
onClick = context.eventHandler { state = state.copy(showOuterSheet = false) }
),
context.toggleInnerSheetButton(renderState),
color = android.R.color.holo_green_light
)
)
}

val innerSheet = if (!renderState.showInnerSheet) {
null
} else {
FullScreenOverlay(
ButtonBar(
Button(
name = R.string.CLOSE,
onClick = context.eventHandler { state = state.copy(showInnerSheet = false) }
),
toggleTopBarButton,
toggleBottomBarButton,
color = android.R.color.holo_red_light
)
)
}
val bodyBarButtons = ButtonBar(toggleTopBarButton, toggleBottomBarButton)

return BodyAndOverlaysScreen(
name = "outer",
overlays = listOfNotNull(outerSheet),
body = TopAndBottomBarsScreen(
topBar = if (!renderState.showTopBar) null else context.topBottomBar(renderState),
content = BodyAndOverlaysScreen(
name = "inner",
body = bodyBarButtons,
overlays = listOfNotNull(innerSheet)
),
bottomBar = if (!renderState.showBottomBar) null else context.topBottomBar(renderState)
)
)
}

override fun snapshotState(state: State) = null

private fun RenderContext.topBottomBar(
renderState: State
) = ButtonBar(
toggleInnerSheetButton(renderState),
Button(
name = R.string.COVER_ALL,
onClick = eventHandler { state = state.copy(showOuterSheet = true) }
)
)

private fun RenderContext.toggleInnerSheetButton(renderState: State) =
Button(
name = if (renderState.showInnerSheet) R.string.REVEAL_BODY else R.string.COVER_BODY,
onClick = eventHandler {
state = state.copy(showInnerSheet = !state.showInnerSheet)
}
)
}
Loading