Skip to content

Commit

Permalink
Introduces WorkflowActivityRunner.
Browse files Browse the repository at this point in the history
One stop shopping to drive your activity from a workflow tree.
See the kdoc on [FragmentActivity.setContentWorkflow] in
[WorkflowActivityRunner.kt] for details.

This API is seriously locked down, suitable only for driving an entire
activity. We can open it up as other use cases emerge -- perhaps
a `WorkflowViewRunner` for embedding in legacy apps.

Closes #217
  • Loading branch information
rjrjr committed Apr 2, 2019
1 parent 6f65bbd commit a324994
Show file tree
Hide file tree
Showing 15 changed files with 405 additions and 112 deletions.
8 changes: 5 additions & 3 deletions kotlin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ buildscript {
ext.versions = [
'agp': '3.2.1',
'buildTools': '28.0.3',
'compileSdk': '27',
'compileSdk': '28',
'lifecycle': '1.1.1',
'minSdk': 21,
'targetSdk': 26,
'targetSdk': 28,
'sourceCompatibility': JavaVersion.VERSION_1_8,
'targetCompatibility': JavaVersion.VERSION_1_8,

'coordinators': '0.4',
'rxandroid2': '2.1.0',
'support': '26.0.2',
'support': '28.0.0',
'timber': '4.7.1',

'assertj': '2.6.0',
Expand All @@ -50,6 +51,7 @@ buildscript {
'coordinators': "com.squareup.coordinators:coordinators:${versions.coordinators}",
'constraint_layout': "com.android.support.constraint:constraint-layout:1.1.0",
'design': "com.android.support:design:${versions.support}",
'lifecycle': "android.arch.lifecycle:extensions:${versions.lifecycle}",
'rxandroid2': "io.reactivex.rxjava2:rxandroid:${versions.rxandroid2}",
'timber': "com.jakewharton.timber:timber:${versions.timber}",
'transition': "com.android.support:transition:${versions.support}",
Expand Down
1 change: 1 addition & 0 deletions kotlin/samples/tictactoe/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies {
implementation deps.appcompatv7
implementation deps.constraint_layout
implementation deps.design
implementation deps.lifecycle
implementation deps.kotlin.coroutines.rx2
implementation deps.kotlin.reflect
implementation deps.okio
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,87 +17,49 @@ package com.squareup.sample.mainactivity

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.View
import com.squareup.sample.authworkflow.AuthViewBindings
import com.squareup.sample.gameworkflow.RunGameScreen
import com.squareup.sample.gameworkflow.TicTacToeViewBindings
import com.squareup.sample.panel.PanelContainer
import com.squareup.workflow.ui.AlertContainerScreen
import com.squareup.workflow.ui.HandlesBack
import com.squareup.workflow.ui.ModalContainer
import com.squareup.workflow.ui.ViewBinding
import com.squareup.workflow.ui.PickledWorkflow
import com.squareup.workflow.ui.ViewRegistry
import com.squareup.workflow.ui.WorkflowActivityRunner
import com.squareup.workflow.ui.backstack.BackStackContainer
import com.squareup.workflow.ui.backstack.PushPopEffect
import com.squareup.workflow.Snapshot
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import kotlin.reflect.jvm.jvmName
import com.squareup.workflow.ui.setContentWorkflow

/**
* Prototype Android integration. Demonstrates:
*
* - preserving workflow state via the activity bundle
* - simple two layer container, with body views and dialogs
* - TODO: customizing stock views via wrapping (when we add a logout button to each game view)
*/
class MainActivity : AppCompatActivity() {
private lateinit var component: MainComponent

private lateinit var content: View

private val subs = CompositeDisposable()

private var latestSnapshot = Snapshot.EMPTY
private lateinit var workflowViewModel: WorkflowActivityRunner<*, *>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val restored = savedInstanceState?.getParcelable<PickledWorkflow>(WORKFLOW_BUNDLE_KEY)
component = lastCustomNonConfigurationInstance as? MainComponent
?: MainComponent()

val snapshot = savedInstanceState?.getParcelable<ParceledSnapshot>(SNAPSHOT_NAME)
?.snapshot
val updates = component.updates(snapshot)

val screens = updates
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { latestSnapshot = it.snapshot }
.map { it.rendering }

val viewRegistry = buildViewRegistry()
val rootViewBinding: ViewBinding<RunGameScreen> =
viewRegistry.getBinding(AlertContainerScreen::class.jvmName)

content = rootViewBinding.buildView(screens, viewRegistry, this)
.apply { setContentView(this) }
workflowViewModel = setContentWorkflow(viewRegistry, component.mainWorkflow, restored)
}

override fun onBackPressed() {
if (!HandlesBack.Helper.onBackPressed(content)) super.onBackPressed()
if (!workflowViewModel.onBackPressed(this)) super.onBackPressed()
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(SNAPSHOT_NAME, ParceledSnapshot(latestSnapshot))
}

override fun onDestroy() {
subs.clear()
super.onDestroy()
outState.putParcelable(WORKFLOW_BUNDLE_KEY, workflowViewModel.asParcelable())
}

override fun onRetainCustomNonConfigurationInstance(): Any = component

private fun buildViewRegistry(): ViewRegistry {
return ViewRegistry(
private companion object {
const val WORKFLOW_BUNDLE_KEY = "workflow"

val viewRegistry = ViewRegistry(
BackStackContainer,
ModalContainer.forAlertContainerScreen(),
PanelContainer
) + AuthViewBindings + TicTacToeViewBindings + PushPopEffect
}

private companion object {
val SNAPSHOT_NAME = MainActivity::class.jvmName + "-snapshot"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,19 @@ import com.squareup.sample.authworkflow.RealAuthWorkflow
import com.squareup.sample.gameworkflow.RealGameLog
import com.squareup.sample.gameworkflow.RealRunGameWorkflow
import com.squareup.sample.gameworkflow.RealTakeTurnsWorkflow
import com.squareup.sample.gameworkflow.RunGameScreen
import com.squareup.sample.gameworkflow.RunGameWorkflow
import com.squareup.sample.gameworkflow.TakeTurnsWorkflow
import com.squareup.sample.mainworkflow.MainWorkflow
import com.squareup.workflow.Snapshot
import com.squareup.workflow.WorkflowHost
import com.squareup.workflow.WorkflowHost.Update
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers.mainThread
import kotlinx.coroutines.experimental.Dispatchers
import kotlinx.coroutines.experimental.rx2.asObservable
import timber.log.Timber

/**
* Pretend generated code of a pretend DI framework.
*/
internal class MainComponent {

private val workflowHostFactory = WorkflowHost.Factory(Dispatchers.Unconfined)

private val authService = AuthService()

private fun mainWorkflow() = MainWorkflow(authWorkflow(), gameWorkflow())

private fun authWorkflow(): AuthWorkflow = RealAuthWorkflow(authService)

private fun gameLog() = RealGameLog(mainThread())
Expand All @@ -53,25 +42,7 @@ internal class MainComponent {

private fun takeTurnsWorkflow(): TakeTurnsWorkflow = RealTakeTurnsWorkflow()

private fun workflowHost(snapshot: Snapshot?) = workflowHostFactory.run(mainWorkflow(), snapshot)

private var updates: Observable<Update<Unit, RunGameScreen>>? = null

fun updates(snapshot: Snapshot?): Observable<Update<Unit, RunGameScreen>> {
if (updates == null) {
val host = workflowHost(snapshot)

@Suppress("EXPERIMENTAL_FEATURE_WARNING")
updates = host.updates.asObservable(Dispatchers.Unconfined)
.doOnNext { Timber.d("showing: %s", it.rendering) }
.replay(1)
.autoConnect()

// autoConnect() is leaky (it's never disposed), but we want it to run
// forever so ¯\_(ツ)_/¯.
}
return updates!!
}
val mainWorkflow = MainWorkflow(authWorkflow(), gameWorkflow())

companion object {
init {
Expand Down
6 changes: 6 additions & 0 deletions kotlin/workflow-ui-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@ targetCompatibility = JavaVersion.VERSION_1_7
android rootProject.ext.defaultAndroidConfig

dependencies {
api project(':workflow-core')
api project(':workflow-ui-core')

api deps.coordinators
api deps.kotlin.stdLib.jdk6
api deps.lifecycle
api deps.rxjava2.rxjava2
api deps.transition

implementation project(':workflow-runtime')
implementation deps.appcompatv7
implementation deps.kotlin.coroutines.core
implementation deps.kotlin.coroutines.rx2
implementation deps.kotlin.reflect
implementation deps.rxandroid2
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ import com.squareup.workflow.ui.HandlesBack.Helper.setConditionalBackHandler
/**
* Implemented by objects that want the option to intercept back button taps.
* Can be implemented by [View] subclasses, or can be attached to a stock view via
* [Helper].
* [Helper.setBackHandler].
*
* When implemented by a container view, the [onBackPressed] methods or tags of their
* subviews should be invoked first.
* When implemented by a container view, the [onBackPressed] methods or tags of its
* subviews should be invoked first, via [Helper.onBackPressed]
*
* The typical flow of back button handling starts in the [android.app.Activity.onBackPressed]
* calling [onBackPressed] on its content view. Each view in turn delegates to its
* child views to give them first say.
* To kick things off, override [android.app.Activity.onBackPressed] to call
* [WorkflowActivityRunner.onBackPressed]:
*
* override fun onBackPressed() {
* if (!workflowViewModel.onBackPressed(this)) super.onBackPressed()
* }
*/
interface HandlesBack {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ abstract class ModalContainer<M : Any>

override fun onSaveInstanceState(): Parcelable {
return SavedState(
super.onSaveInstanceState(),
super.onSaveInstanceState()!!,
SparseArray<Parcelable>().also { array -> base?.saveHierarchyState(array) },
dialogs.map { it.save() }
)
Expand Down Expand Up @@ -196,8 +196,8 @@ abstract class ModalContainer<M : Any>

companion object CREATOR : Creator<TypeAndBundle> {
override fun createFromParcel(parcel: Parcel): TypeAndBundle {
val type = parcel.readString()
val bundle = parcel.readBundle(TypeAndBundle::class.java.classLoader)
val type = parcel.readString()!!
val bundle = parcel.readBundle(TypeAndBundle::class.java.classLoader)!!
return TypeAndBundle(type, bundle)
}

Expand All @@ -210,13 +210,13 @@ abstract class ModalContainer<M : Any>
val dialog: Dialog
) {
fun save(): TypeAndBundle {
val saved = dialog.window.saveHierarchyState()
val saved = dialog.window!!.saveHierarchyState()
return TypeAndBundle(screen::class.jvmName, saved)
}

fun restore(typeAndBundle: TypeAndBundle) {
if (screen::class.jvmName == typeAndBundle.screenType) {
dialog.window.restoreHierarchyState(typeAndBundle.bundle)
dialog.window!!.restoreHierarchyState(typeAndBundle.bundle)
}
}
}
Expand Down Expand Up @@ -244,7 +244,7 @@ abstract class ModalContainer<M : Any>

private class SavedState : BaseSavedState {
constructor(
superState: Parcelable,
superState: Parcelable?,
bodyState: SparseArray<Parcelable>,
dialogBundles: List<TypeAndBundle>
) : super(superState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ internal class ModalViewContainer
return Dialog(context, dialogThemeResId).apply {
setCancelable(false)
setContentView(modalDecorator(view))
window.setLayout(WRAP_CONTENT, WRAP_CONTENT)
window!!.setLayout(WRAP_CONTENT, WRAP_CONTENT)

if (dialogThemeResId == 0) {
// If we don't set or clear the background drawable, the window cannot go full bleed.
window.setBackgroundDrawable(null)
window!!.setBackgroundDrawable(null)
}
show()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017 Square Inc.
* Copyright 2019 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -13,28 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.sample.mainactivity
package com.squareup.workflow.ui

import android.os.Parcel
import android.os.Parcelable
import com.squareup.workflow.Snapshot
import okio.ByteString

internal class ParceledSnapshot(val snapshot: Snapshot) : Parcelable {

/**
* A [Parcelable] snapshot of the most recent state of a [Workflow] managed by
* [WorkflowActivityRunner], for use in an `Activity`'s persistence `Bundle`.
* See [WorkflowActivityRunner.asParcelable] for details.
*/
class PickledWorkflow(internal val snapshot: Snapshot) : Parcelable {
override fun describeContents(): Int = 0

override fun writeToParcel(
dest: Parcel,
flags: Int
) = dest.writeByteArray(snapshot.bytes.toByteArray())

companion object CREATOR : Parcelable.Creator<ParceledSnapshot> {
override fun createFromParcel(parcel: Parcel): ParceledSnapshot {
val bytes = parcel.createByteArray()
return ParceledSnapshot(Snapshot.of(ByteString.of(*bytes)))
companion object CREATOR : Parcelable.Creator<PickledWorkflow> {
override fun createFromParcel(parcel: Parcel): PickledWorkflow {
val bytes = parcel.createByteArray()!!
return PickledWorkflow(Snapshot.of(ByteString.of(*bytes)))
}

override fun newArray(size: Int): Array<ParceledSnapshot?> = arrayOfNulls(size)
override fun newArray(size: Int): Array<PickledWorkflow?> = arrayOfNulls(size)
}
}
Loading

0 comments on commit a324994

Please sign in to comment.