Skip to content
This repository was archived by the owner on Feb 5, 2021. It is now read-only.
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
17 changes: 17 additions & 0 deletions core-compose/api/core-compose.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
public final class com/squareup/workflow/compose/ComposeRendering {
public static final field Companion Lcom/squareup/workflow/compose/ComposeRendering$Companion;
public static final fun <clinit> ()V
public fun <init> (Lkotlin/jvm/functions/Function2;)V
}

public final class com/squareup/workflow/compose/ComposeRendering$Companion {
public final fun getFactory ()Lcom/squareup/workflow/ui/ViewFactory;
public final fun getNoopRendering ()Lcom/squareup/workflow/compose/ComposeRendering;
}

public abstract class com/squareup/workflow/compose/ComposeWorkflow : com/squareup/workflow/Workflow {
public fun <init> ()V
public fun asStatefulWorkflow ()Lcom/squareup/workflow/StatefulWorkflow;
public abstract fun render (Ljava/lang/Object;Lcom/squareup/workflow/Sink;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/compose/Composer;)V
}

public final class com/squareup/workflow/ui/compose/ComposeViewFactory : com/squareup/workflow/ui/ViewFactory {
public fun <init> (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function3;)V
public fun buildView (Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2020 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.
*/
@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry")

package com.squareup.workflow.compose

import androidx.compose.Composable
import com.squareup.workflow.compose.ComposeRendering.Companion.Factory
import com.squareup.workflow.compose.ComposeRendering.Companion.NoopRendering
import com.squareup.workflow.ui.ViewEnvironment
import com.squareup.workflow.ui.ViewFactory
import com.squareup.workflow.ui.compose.bindCompose

/**
* A workflow rendering that renders itself using a [Composable] function.
*
* This is the rendering type of [ComposeWorkflow]. To stub out [ComposeWorkflow]s in `RenderTester`
* tests, use [NoopRendering].
*
* To use this type, make sure your `ViewRegistry` registers [Factory].
*/
class ComposeRendering internal constructor(
internal val render: @Composable() (ViewEnvironment) -> Unit
) {
companion object {
/**
* A [ViewFactory] that renders a [ComposeRendering].
*/
val Factory: ViewFactory<ComposeRendering> = bindCompose { rendering, environment ->
rendering.render(environment)
}

/**
* A [ComposeRendering] that doesn't do anything. Useful for unit testing.
*/
val NoopRendering = ComposeRendering {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2020 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 com.squareup.workflow.compose

import androidx.compose.Composable
import com.squareup.workflow.Sink
import com.squareup.workflow.StatefulWorkflow
import com.squareup.workflow.Workflow
import com.squareup.workflow.ui.ViewEnvironment

/**
* A stateless [Workflow][com.squareup.workflow.Workflow] that [renders][render] itself as
* [Composable] function. Effectively defines an inline
* [bindCompose][com.squareup.workflow.ui.compose.bindCompose].
*
* This workflow does not have access to a [RenderContext][com.squareup.workflow.RenderContext]
* since render contexts are only valid during render passes, and this workflow's [render] method
* is invoked after the render pass, when view bindings are being shown.
*
* While this workflow is "stateless" in the usual workflow sense (it doesn't have a `StateT` type),
* since [render] is a Composable function, it can use all the usual Compose facilities for state
* management.
*/
abstract class ComposeWorkflow<in PropsT, out OutputT : Any> :
Workflow<PropsT, OutputT, ComposeRendering> {

/**
* Renders [props] using Compose. This function will be called to update the UI whenever the
* [props] or [viewEnvironment] changes.
*
* @param props The data to render.
* @param outputSink A [Sink] that can be used from UI event handlers to send outputs to this
* workflow's parent.
* @param viewEnvironment The [ViewEnvironment] passed down through the `ViewBinding` pipeline.
*/
@Composable abstract fun render(
props: PropsT,
outputSink: Sink<OutputT>,
viewEnvironment: ViewEnvironment
)

override fun asStatefulWorkflow(): StatefulWorkflow<PropsT, *, OutputT, ComposeRendering> =
ComposeWorkflowImpl(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2020 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 com.squareup.workflow.compose

import androidx.compose.MutableState
import androidx.compose.StructurallyEqual
import androidx.compose.mutableStateOf
import com.squareup.workflow.RenderContext
import com.squareup.workflow.Sink
import com.squareup.workflow.Snapshot
import com.squareup.workflow.StatefulWorkflow
import com.squareup.workflow.action
import com.squareup.workflow.compose.ComposeWorkflowImpl.State
import com.squareup.workflow.contraMap

internal class ComposeWorkflowImpl<PropsT, OutputT : Any>(
private val workflow: ComposeWorkflow<PropsT, OutputT>
) : StatefulWorkflow<PropsT, State<PropsT, OutputT>, OutputT, ComposeRendering>() {

// This doesn't need to be a @Model, it only gets set once, before the composable ever runs.
class SinkHolder<OutputT>(var sink: Sink<OutputT>? = null)

data class State<PropsT, OutputT>(
val propsHolder: MutableState<PropsT>,
val sinkHolder: SinkHolder<OutputT>,
val rendering: ComposeRendering
)

override fun initialState(
props: PropsT,
snapshot: Snapshot?
): State<PropsT, OutputT> {
val propsHolder = mutableStateOf(props, areEquivalent = StructurallyEqual)
val sinkHolder = SinkHolder<OutputT>()
return State(propsHolder, sinkHolder, ComposeRendering { environment ->
// The sink will get set on the first render pass, so it should never be null.
val sink = sinkHolder.sink!!
// Important: Use the props from the MutableState, _not_ the one passed into render.
workflow.render(propsHolder.value, sink, environment)
})
}

override fun onPropsChanged(
old: PropsT,
new: PropsT,
state: State<PropsT, OutputT>
): State<PropsT, OutputT> {
state.propsHolder.value = new
return state
}

override fun render(
props: PropsT,
state: State<PropsT, OutputT>,
context: RenderContext<State<PropsT, OutputT>, OutputT>
): ComposeRendering {
// The first render pass needs to cache the sink. The sink is reusable, so we can just pass the
// same one every time.
if (state.sinkHolder.sink == null) {
state.sinkHolder.sink = context.actionSink.contraMap(::forwardOutput)
}

// onPropsChanged will ensure the rendering is re-composed when the props changes.
return state.rendering
}

// Compiler bug doesn't let us call Snapshot.EMPTY.
override fun snapshotState(state: State<PropsT, OutputT>): Snapshot = Snapshot.of("")

private fun forwardOutput(output: OutputT) = action { setOutput(output) }
}
51 changes: 51 additions & 0 deletions samples/hello-compose-rendering/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

/*
* Copyright 2020 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.
*/
plugins {
id("com.android.application")
kotlin("android")
}

apply(from = rootProject.file(".buildscript/configure-android-defaults.gradle"))
apply(from = rootProject.file(".buildscript/android-sample-app.gradle"))

android {
defaultConfig {
applicationId = "com.squareup.sample.hellocomposerendering"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}

apply(from = rootProject.file(".buildscript/configure-compose.gradle"))
tasks.withType<KotlinCompile> {
kotlinOptions.apiVersion = "1.3"
}

dependencies {
implementation(project(":core-compose"))
implementation(Dependencies.AndroidX.appcompat)
implementation(Dependencies.Compose.foundation)
implementation(Dependencies.Compose.layout)
implementation(Dependencies.Compose.material)
implementation(Dependencies.Workflow.core)
implementation(Dependencies.Workflow.runtime)

androidTestImplementation(Dependencies.Compose.test)
androidTestImplementation(Dependencies.Test.AndroidX.junitExt)
androidTestImplementation(Dependencies.Test.junit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2020 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 com.squareup.sample.hellocomposerendering

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.ui.test.android.AndroidComposeTestRule
import androidx.ui.test.assertIsDisplayed
import androidx.ui.test.doClick
import androidx.ui.test.findByText
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class HelloComposeRenderingTest {

// Launches the activity.
@Rule @JvmField val composeRule = AndroidComposeTestRule<HelloComposeRenderingActivity>()

@Test fun togglesBetweenStates() {
findByText("Hello")
.assertIsDisplayed()
.doClick()
findByText("Goodbye")
.assertIsDisplayed()
.doClick()
findByText("Hello")
.assertIsDisplayed()
}
}
36 changes: 36 additions & 0 deletions samples/hello-compose-rendering/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2020 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.squareup.sample.hellocomposerendering">

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

<activity android:name=".HelloComposeRenderingActivity">

<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,38 @@
/*
* Copyright 2020 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 com.squareup.sample.hellocomposerendering

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.squareup.workflow.compose.ComposeRendering
import com.squareup.workflow.diagnostic.SimpleLoggingDiagnosticListener
import com.squareup.workflow.ui.ViewRegistry
import com.squareup.workflow.ui.WorkflowRunner
import com.squareup.workflow.ui.setContentWorkflow

private val viewRegistry = ViewRegistry(ComposeRendering.Factory)

class HelloComposeRenderingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentWorkflow(viewRegistry) {
WorkflowRunner.Config(
HelloWorkflow,
diagnosticListener = SimpleLoggingDiagnosticListener()
)
}
}
}
Loading