diff --git a/android/demos/compose/build.gradle b/android/demos/compose/build.gradle
new file mode 100644
index 000000000..6494cbd8b
--- /dev/null
+++ b/android/demos/compose/build.gradle
@@ -0,0 +1,67 @@
+apply plugin: 'com.android.application'
+apply plugin: 'org.jetbrains.kotlin.android'
+apply plugin: "org.jetbrains.kotlin.kapt"
+
+android {
+ compileSdkVersion deps.build.compileSdkVersion
+ buildToolsVersion deps.build.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion deps.build.minSdkVersion
+ targetSdkVersion deps.build.targetSdkVersion
+ applicationId "com.uber.rib.compose"
+ versionCode 1
+ versionName "1.0"
+ }
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion "${deps.versions.androidx.compose}"
+ }
+ compileOptions {
+ sourceCompatibility deps.build.javaVersion
+ targetCompatibility deps.build.javaVersion
+ }
+
+ // No need for lint. This is just a tutorial.
+ lintOptions {
+ abortOnError false
+ quiet true
+ }
+}
+
+dependencies {
+ kapt deps.uber.motifCompiler
+ implementation project(":libraries:rib-android")
+ implementation project(":libraries:rib-android-compose")
+ implementation deps.androidx.activityCompose
+ implementation deps.androidx.annotations
+ implementation deps.androidx.appcompat
+ implementation deps.androidx.composeAnimation
+ implementation deps.androidx.composeFoundation
+ implementation deps.androidx.composeMaterial
+ implementation deps.androidx.composeNavigation
+ implementation deps.androidx.composeRuntimeRxJava2
+ implementation deps.androidx.composeUi
+ implementation deps.androidx.composeViewModel
+ implementation deps.androidx.composeUiTooling
+ implementation deps.external.rxandroid2
+ implementation deps.kotlin.coroutines
+ implementation deps.kotlin.coroutinesAndroid
+ implementation deps.kotlin.coroutinesRx2
+ implementation deps.kotlin.stdlib
+ implementation deps.uber.autodisposeCoroutines
+ implementation deps.uber.motif
+
+
+ // Flipper Debug tool integration
+ debugImplementation 'com.facebook.flipper:flipper:0.93.0'
+ debugImplementation 'com.facebook.soloader:soloader:0.10.1'
+ releaseImplementation 'com.facebook.flipper:flipper-noop:0.93.0'
+
+ // Flipper RIBs plugin
+ implementation project(":tooling:rib-flipper-plugin")
+
+ testImplementation deps.test.junit
+}
diff --git a/android/demos/compose/src/main/AndroidManifest.xml b/android/demos/compose/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..4e71a9a33
--- /dev/null
+++ b/android/demos/compose/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/ComposeApplication.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/ComposeApplication.kt
new file mode 100644
index 000000000..00743aff4
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/ComposeApplication.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose
+
+import android.app.Application
+import com.facebook.flipper.android.AndroidFlipperClient
+import com.facebook.flipper.android.utils.FlipperUtils
+import com.facebook.flipper.core.FlipperClient
+import com.facebook.flipper.plugins.inspector.DescriptorMapping
+import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
+import com.facebook.soloader.SoLoader
+import com.uber.rib.flipper.RibTreePlugin
+
+class ComposeApplication : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ SoLoader.init(this, false)
+
+ if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
+ val client: FlipperClient = AndroidFlipperClient.getInstance(this)
+ client.addPlugin(RibTreePlugin())
+ client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
+ client.start()
+ }
+ }
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootActivity.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootActivity.kt
new file mode 100644
index 000000000..d3352d77b
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootActivity.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root
+
+import android.view.ViewGroup
+import com.uber.rib.core.RibActivity
+import com.uber.rib.core.ViewRouter
+import motif.Creatable
+import motif.Expose
+import motif.NoDependencies
+import motif.ScopeFactory
+
+class RootActivity : RibActivity() {
+
+ override fun createRouter(parentViewGroup: ViewGroup): ViewRouter<*, *> {
+ return ScopeFactory.create(Parent::class.java)
+ .rootScope(this, findViewById(android.R.id.content))
+ .router()
+ }
+
+ @motif.Scope
+ interface Parent : Creatable {
+ fun rootScope(@Expose activity: RibActivity, parentViewGroup: ViewGroup): RootScope
+ }
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootInteractor.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootInteractor.kt
new file mode 100644
index 000000000..ec8c3821a
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootInteractor.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root
+
+import com.uber.rib.core.BasicInteractor
+import com.uber.rib.core.EmptyPresenter
+
+class RootInteractor(presenter: EmptyPresenter) : BasicInteractor(presenter)
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootRouter.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootRouter.kt
new file mode 100644
index 000000000..5a926b38b
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootRouter.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root
+
+import com.uber.rib.compose.root.main.MainRouter
+import com.uber.rib.core.BasicViewRouter
+
+class RootRouter(
+ view: RootView,
+ interactor: RootInteractor,
+ private val scope: RootScope
+) : BasicViewRouter(view, interactor) {
+
+ private var mainRouter: MainRouter? = null
+
+ override fun willAttach() {
+ attachMain()
+ }
+
+ override fun willDetach() {
+ detachMain()
+ }
+
+ private fun attachMain() {
+ if (mainRouter == null) {
+ mainRouter = scope.mainScope(view).router().also {
+ attachChild(it)
+ this@RootRouter.view.addView(it.view)
+ }
+ }
+ }
+
+ private fun detachMain() {
+ mainRouter?.let {
+ this@RootRouter.view.removeView(it.view)
+ detachChild(it)
+ }
+ }
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootScope.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootScope.kt
new file mode 100644
index 000000000..ca08b5d06
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootScope.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root
+
+import android.view.ViewGroup
+import androidx.lifecycle.ViewTreeLifecycleOwner
+import androidx.savedstate.ViewTreeSavedStateRegistryOwner
+import com.uber.rib.compose.root.main.MainScope
+import com.uber.rib.compose.util.AnalyticsClient
+import com.uber.rib.compose.util.AnalyticsClientImpl
+import com.uber.rib.compose.util.ExperimentClient
+import com.uber.rib.compose.util.ExperimentClientImpl
+import com.uber.rib.compose.util.LoggerClient
+import com.uber.rib.compose.util.LoggerClientImpl
+import com.uber.rib.core.EmptyPresenter
+import com.uber.rib.core.RibActivity
+import motif.Expose
+
+@motif.Scope
+interface RootScope {
+ fun router(): RootRouter
+
+ fun mainScope(parentViewGroup: ViewGroup): MainScope
+
+ @motif.Objects
+ abstract class Objects {
+ abstract fun router(): RootRouter
+
+ abstract fun interactor(): RootInteractor
+
+ abstract fun presenter(): EmptyPresenter
+
+ fun view(parentViewGroup: ViewGroup, activity: RibActivity): RootView {
+ return RootView(parentViewGroup.context).apply {
+ ViewTreeLifecycleOwner.set(this, activity)
+ ViewTreeSavedStateRegistryOwner.set(this, activity)
+ }
+ }
+
+ @Expose
+ fun analyticsClient(activity: RibActivity): AnalyticsClient {
+ return AnalyticsClientImpl(activity.application)
+ }
+
+ @Expose
+ fun experimentClient(activity: RibActivity): ExperimentClient {
+ return ExperimentClientImpl(activity.application)
+ }
+
+ @Expose
+ fun loggerClient(activity: RibActivity): LoggerClient {
+ return LoggerClientImpl(activity.application)
+ }
+ }
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootView.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootView.kt
new file mode 100644
index 000000000..6a05711c0
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/RootView.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root
+
+import android.content.Context
+import android.graphics.Color
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import android.widget.TextView
+
+class RootView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : FrameLayout(context, attrs, defStyle) {
+
+ init {
+ setBackgroundColor(Color.RED)
+ addView(
+ TextView(context).apply {
+ text = "root (view)"
+ setTextColor(Color.WHITE)
+ }
+ )
+ }
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/AuthStream.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/AuthStream.kt
new file mode 100644
index 000000000..bc144015b
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/AuthStream.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root.main
+
+import com.jakewharton.rxrelay2.BehaviorRelay
+import io.reactivex.Observable
+
+class AuthStream {
+ private val authRelay = BehaviorRelay.createDefault(AuthInfo(false, "", ""))
+
+ fun observe(): Observable = authRelay.hide()
+
+ fun accept(value: AuthInfo) {
+ authRelay.accept(value)
+ }
+}
+
+data class AuthInfo(val isLoggedIn: Boolean, val playerOne: String = "", val playerTwo: String = "")
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainInteractor.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainInteractor.kt
new file mode 100644
index 000000000..f5de85c43
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainInteractor.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root.main
+
+import com.uber.autodispose.autoDispose
+import com.uber.rib.core.BasicInteractor
+import com.uber.rib.core.Bundle
+import com.uber.rib.core.ComposePresenter
+import io.reactivex.android.schedulers.AndroidSchedulers.mainThread
+import io.reactivex.schedulers.Schedulers
+
+class MainInteractor(
+ presenter: ComposePresenter,
+ private val authStream: AuthStream,
+ private val childContent: MainRouter.ChildContent
+) : BasicInteractor(presenter) {
+
+ override fun didBecomeActive(savedInstanceState: Bundle?) {
+ super.didBecomeActive(savedInstanceState)
+ router.view.setContent { MainView(childContent = childContent) }
+ authStream.observe()
+ .subscribeOn(Schedulers.io())
+ .observeOn(mainThread())
+ .autoDispose(this)
+ .subscribe {
+ if (it.isLoggedIn) {
+ router.detachLoggedOut()
+ router.attachLoggedIn(it)
+ } else {
+ router.detachLoggedIn()
+ router.attachLoggedOut()
+ }
+ }
+ }
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainRouter.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainRouter.kt
new file mode 100644
index 000000000..749925c5d
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainRouter.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root.main
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.ComposeView
+import com.uber.rib.compose.root.main.logged_in.LoggedInRouter
+import com.uber.rib.compose.root.main.logged_out.LoggedOutRouter
+import com.uber.rib.core.BasicViewRouter
+
+class MainRouter(
+ view: ComposeView,
+ interactor: MainInteractor,
+ private val scope: MainScope,
+ private val childContent: MainRouter.ChildContent
+) : BasicViewRouter(view, interactor) {
+
+ private var loggedOutRouter: LoggedOutRouter? = null
+ private var loggedInRouter: LoggedInRouter? = null
+
+ internal fun attachLoggedOut() {
+ if (loggedOutRouter == null) {
+ loggedOutRouter = scope.loggedOutScope(view).router().also {
+ attachChild(it)
+ childContent.fullScreenContent = it.presenter.composable
+ }
+ }
+ }
+
+ internal fun attachLoggedIn(authInfo: AuthInfo) {
+ if (loggedInRouter == null) {
+ loggedInRouter = scope.loggedInScope(view, authInfo).router().also {
+ attachChild(it)
+ childContent.fullScreenContent = it.presenter.composable
+ }
+ }
+ }
+
+ internal fun detachLoggedOut() {
+ loggedOutRouter?.let {
+ childContent.fullScreenContent = null
+ detachChild(it)
+ }
+ loggedOutRouter = null
+ }
+
+ internal fun detachLoggedIn() {
+ loggedInRouter?.let {
+ childContent.fullScreenContent = null
+ detachChild(it)
+ }
+ loggedInRouter = null
+ }
+
+ class ChildContent {
+ internal var fullScreenContent: (@Composable () -> Unit)? by mutableStateOf(null)
+ }
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainScope.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainScope.kt
new file mode 100644
index 000000000..e3fe00119
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainScope.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root.main
+
+import android.view.ViewGroup
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.ComposeView
+import androidx.lifecycle.ViewTreeLifecycleOwner
+import androidx.savedstate.ViewTreeSavedStateRegistryOwner
+import com.uber.rib.compose.root.main.logged_in.LoggedInScope
+import com.uber.rib.compose.root.main.logged_out.LoggedOutScope
+import com.uber.rib.compose.util.AnalyticsClient
+import com.uber.rib.compose.util.ExperimentClient
+import com.uber.rib.compose.util.LoggerClient
+import com.uber.rib.core.ComposePresenter
+import com.uber.rib.core.RibActivity
+import motif.Expose
+
+@motif.Scope
+interface MainScope {
+ fun router(): MainRouter
+
+ fun loggedOutScope(parentViewGroup: ViewGroup): LoggedOutScope
+
+ fun loggedInScope(parentViewGroup: ViewGroup, authInfo: AuthInfo): LoggedInScope
+
+ @motif.Objects
+ abstract class Objects {
+ abstract fun router(): MainRouter
+
+ abstract fun interactor(): MainInteractor
+
+ fun presenter(
+ childContent: MainRouter.ChildContent,
+ analyticsClient: AnalyticsClient,
+ experimentClient: ExperimentClient,
+ loggerClient: LoggerClient
+ ): ComposePresenter {
+ return object : ComposePresenter() {
+ override val composable = @Composable {
+ MainView(childContent)
+// CustomClientProvider(
+// analyticsClient = analyticsClient,
+// experimentClient = experimentClient,
+// loggerClient = loggerClient
+// ) {
+//
+// }
+ }
+ }
+ }
+
+ fun view(parentViewGroup: ViewGroup, activity: RibActivity, presenter: ComposePresenter): ComposeView {
+ return ComposeView(parentViewGroup.context).apply {
+ ViewTreeLifecycleOwner.set(this, activity)
+ ViewTreeSavedStateRegistryOwner.set(this, activity)
+ }
+ }
+
+ abstract fun childContent(): MainRouter.ChildContent
+
+ @Expose
+ abstract fun authStream(): AuthStream
+ }
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainView.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainView.kt
new file mode 100644
index 000000000..7dc75ac86
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/MainView.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root.main
+
+import android.widget.FrameLayout
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.uber.rib.compose.R
+
+@Composable
+fun MainView(childContent: MainRouter.ChildContent) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(all = 4.dp)
+ .padding(top = 14.dp)
+ .background(Color(0xFFFFA500))
+ ) {
+ Text("Main RIB (Compose w/ CompView)")
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1.0f)
+ .padding(4.dp)
+ .background(Color.Yellow)
+ ) {
+ if (childContent.fullScreenContent != null) {
+ childContent.fullScreenContent?.invoke()
+ } else {
+ AndroidView(
+ modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
+ factory = { context ->
+ FrameLayout(context).apply {
+ id = R.id.login_logout_container
+ setBackgroundColor(android.graphics.Color.DKGRAY)
+ }
+ }
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun MainViewPreview() {
+ MainView(MainRouter.ChildContent())
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInEvent.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInEvent.kt
new file mode 100644
index 000000000..64aa66907
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInEvent.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root.main.logged_in
+
+sealed class LoggedInEvent {
+ object LogOutClick : LoggedInEvent()
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInInteractor.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInInteractor.kt
new file mode 100644
index 000000000..ca4dc22af
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInInteractor.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root.main.logged_in
+
+import com.uber.autodispose.autoDispose
+import com.uber.rib.compose.root.main.AuthInfo
+import com.uber.rib.compose.root.main.AuthStream
+import com.uber.rib.compose.root.main.logged_in.off_game.OffGameInteractor
+import com.uber.rib.compose.root.main.logged_in.tic_tac_toe.TicTacToeInteractor
+import com.uber.rib.compose.util.EventStream
+import com.uber.rib.core.BasicInteractor
+import com.uber.rib.core.Bundle
+import com.uber.rib.core.ComposePresenter
+import io.reactivex.rxkotlin.ofType
+
+class LoggedInInteractor(
+ presenter: ComposePresenter,
+ private val authInfo: AuthInfo,
+ private val authStream: AuthStream,
+ private val eventStream: EventStream,
+ private val scoreStream: ScoreStream
+) : BasicInteractor(presenter),
+ OffGameInteractor.Listener,
+ TicTacToeInteractor.Listener {
+
+ override fun didBecomeActive(savedInstanceState: Bundle?) {
+ super.didBecomeActive(savedInstanceState)
+ eventStream.observe()
+ .ofType()
+ .autoDispose(this)
+ .subscribe { authStream.accept(AuthInfo(false)) }
+
+ router.attachOffGame(authInfo)
+ }
+
+ override fun onStartGame() {
+ router.detachOffGame()
+ router.attachTicTacToe(authInfo)
+ }
+
+ override fun onGameWon(winner: String?) {
+ if (winner != null) {
+ scoreStream.addVictory(winner)
+ }
+
+ router.detachTicTacToe()
+ router.attachOffGame(authInfo)
+ }
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInRouter.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInRouter.kt
new file mode 100644
index 000000000..9b4beb58f
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInRouter.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root.main.logged_in
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.uber.rib.compose.root.main.AuthInfo
+import com.uber.rib.compose.root.main.logged_in.off_game.OffGameRouter
+import com.uber.rib.compose.root.main.logged_in.tic_tac_toe.TicTacToeRouter
+import com.uber.rib.core.BasicComposeRouter
+import com.uber.rib.core.ComposePresenter
+
+class LoggedInRouter(
+ presenter: ComposePresenter,
+ interactor: LoggedInInteractor,
+ private val scope: LoggedInScope,
+ private val childContent: ChildContent
+) : BasicComposeRouter(presenter, interactor) {
+
+ private var offGameRouter: OffGameRouter? = null
+ private var ticTacToeRouter: TicTacToeRouter? = null
+
+ internal fun attachOffGame(authInfo: AuthInfo) {
+ if (offGameRouter == null) {
+ offGameRouter = scope.offGameScope(authInfo).router().also {
+ attachChild(it)
+ childContent.fullScreenContent = it.presenter.composable
+ }
+ }
+ }
+
+ internal fun attachTicTacToe(authInfo: AuthInfo) {
+ if (ticTacToeRouter == null) {
+ ticTacToeRouter = scope.ticTacToeScope(authInfo).router().also {
+ attachChild(it)
+ childContent.fullScreenContent = it.presenter.composable
+ }
+ }
+ }
+
+ internal fun detachOffGame() {
+ offGameRouter?.let {
+ detachChild(it)
+ }
+ offGameRouter = null
+ childContent.fullScreenContent = null
+ }
+
+ internal fun detachTicTacToe() {
+ ticTacToeRouter?.let {
+ detachChild(it)
+ }
+ ticTacToeRouter = null
+ childContent.fullScreenContent = null
+ }
+
+ override fun willDetach() {
+ detachTicTacToe()
+ super.willDetach()
+ }
+
+ class ChildContent {
+ internal var fullScreenContent: (@Composable () -> Unit)? by mutableStateOf(null)
+ }
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInScope.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInScope.kt
new file mode 100644
index 000000000..059cee0ac
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInScope.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root.main.logged_in
+
+import androidx.compose.runtime.Composable
+import com.uber.rib.compose.root.main.AuthInfo
+import com.uber.rib.compose.root.main.logged_in.off_game.OffGameInteractor
+import com.uber.rib.compose.root.main.logged_in.off_game.OffGameScope
+import com.uber.rib.compose.root.main.logged_in.tic_tac_toe.TicTacToeInteractor
+import com.uber.rib.compose.root.main.logged_in.tic_tac_toe.TicTacToeScope
+import com.uber.rib.compose.util.EventStream
+import com.uber.rib.core.ComposePresenter
+import motif.Expose
+
+@motif.Scope
+interface LoggedInScope {
+ fun router(): LoggedInRouter
+
+ fun offGameScope(authInfo: AuthInfo): OffGameScope
+
+ fun ticTacToeScope(authInfo: AuthInfo): TicTacToeScope
+
+ @motif.Objects
+ abstract class Objects {
+ abstract fun router(): LoggedInRouter
+
+ abstract fun interactor(): LoggedInInteractor
+
+ abstract fun childContent(): LoggedInRouter.ChildContent
+
+ fun presenter(eventStream: EventStream, childContent: LoggedInRouter.ChildContent): ComposePresenter {
+ return object : ComposePresenter() {
+ override val composable = @Composable {
+ LoggedInView(eventStream, childContent)
+ }
+ }
+ }
+
+ fun eventStream() = EventStream()
+
+ @Expose
+ fun scoreSteam(authInfo: AuthInfo): ScoreStream {
+ return ScoreStream(authInfo.playerOne, authInfo.playerTwo)
+ }
+
+ @Expose
+ abstract fun startGameListener(interactor: LoggedInInteractor): OffGameInteractor.Listener
+
+ @Expose
+ abstract fun gameWonListener(interactor: LoggedInInteractor): TicTacToeInteractor.Listener
+ }
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInView.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInView.kt
new file mode 100644
index 000000000..60917fd7e
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/LoggedInView.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * 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.uber.rib.compose.root.main.logged_in
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.uber.rib.compose.util.CustomButton
+import com.uber.rib.compose.util.EventStream
+
+@Composable
+fun LoggedInView(
+ eventStream: EventStream,
+ childContent: LoggedInRouter.ChildContent,
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ ) {
+ Text("Logged In! (Compose RIB)")
+ Spacer(Modifier.height(16.dp))
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1.0f)
+ .padding(4.dp)
+ .background(Color.LightGray)
+ ) {
+ childContent.fullScreenContent?.invoke()
+ }
+ CustomButton(
+ analyticsId = "8a570808-07a4",
+ onClick = { eventStream.notify(LoggedInEvent.LogOutClick) },
+ modifier = Modifier.fillMaxWidth().padding(16.dp)
+ ) {
+ Text(text = "LOGOUT")
+ }
+ }
+}
+
+@Preview
+@Composable
+fun LoggedInViewPreview() {
+ LoggedInView(EventStream(), LoggedInRouter.ChildContent())
+}
diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/ScoreStream.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/ScoreStream.kt
new file mode 100644
index 000000000..49dfbbee1
--- /dev/null
+++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/ScoreStream.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017. Uber Technologies
+ *
+ * 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.uber.rib.compose.root.main.logged_in
+
+import com.jakewharton.rxrelay2.BehaviorRelay
+import io.reactivex.Observable
+
+class ScoreStream(playerOne: String, playerTwo: String) {
+
+ private val scoresRelay: BehaviorRelay