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> = BehaviorRelay.createDefault( + mapOf( + playerOne to 0, + playerTwo to 0 + ) + ) + + fun addVictory(userName: String) { + val scores = (scoresRelay.value ?: emptyMap()).toMutableMap() + if (userName in scores) { + scores[userName] = scores[userName]!! + 1 + } + scoresRelay.accept(scores) + } + + fun scores(): Observable> { + return scoresRelay.hide() + } +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameEvent.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameEvent.kt new file mode 100644 index 000000000..b085796d3 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameEvent.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.off_game + +sealed class OffGameEvent { + object StartGame : OffGameEvent() +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameInteractor.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameInteractor.kt new file mode 100644 index 000000000..968d4c9a7 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameInteractor.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.off_game + +import com.uber.autodispose.autoDispose +import com.uber.rib.compose.root.main.logged_in.ScoreStream +import com.uber.rib.compose.util.EventStream +import com.uber.rib.compose.util.StateStream +import com.uber.rib.core.BasicInteractor +import com.uber.rib.core.Bundle +import com.uber.rib.core.ComposePresenter +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxkotlin.ofType + +class OffGameInteractor( + presenter: ComposePresenter, + private val eventStream: EventStream, + private val stateStream: StateStream, + private val scoreStream: ScoreStream, + private val listener: Listener +) : BasicInteractor(presenter) { + + override fun didBecomeActive(savedInstanceState: Bundle?) { + super.didBecomeActive(savedInstanceState) + eventStream.observe() + .ofType() + .autoDispose(this) + .subscribe { + listener.onStartGame() + } + + scoreStream.scores() + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this) + .subscribe { + val currentState = stateStream.current() + stateStream.dispatch( + currentState.copy( + playerOneWins = it[currentState.playerOne] ?: 0, + playerTwoWins = it[currentState.playerTwo] ?: 0, + ) + ) + } + } + + interface Listener { + fun onStartGame() + } +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameRouter.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameRouter.kt new file mode 100644 index 000000000..92300184c --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameRouter.kt @@ -0,0 +1,24 @@ +/* + * 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.off_game + +import com.uber.rib.core.BasicComposeRouter +import com.uber.rib.core.ComposePresenter + +class OffGameRouter( + presenter: ComposePresenter, + interactor: OffGameInteractor +) : BasicComposeRouter(presenter, interactor) diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameScope.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameScope.kt new file mode 100644 index 000000000..af2fa95cb --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameScope.kt @@ -0,0 +1,55 @@ +/* + * 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.off_game + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rxjava2.subscribeAsState +import com.uber.rib.compose.root.main.AuthInfo +import com.uber.rib.compose.util.EventStream +import com.uber.rib.compose.util.StateStream +import com.uber.rib.core.ComposePresenter + +@motif.Scope +interface OffGameScope { + fun router(): OffGameRouter + + @motif.Objects + abstract class Objects { + abstract fun router(): OffGameRouter + + abstract fun interactor(): OffGameInteractor + + fun presenter( + stateStream: StateStream, + eventStream: EventStream + ): ComposePresenter { + return object : ComposePresenter() { + override val composable = @Composable { + OffGameView(stateStream.observe().subscribeAsState(initial = stateStream.current()), eventStream) + } + } + } + + fun eventStream() = EventStream() + + fun stateStream(authInfo: AuthInfo) = StateStream( + OffGameViewModel( + playerOne = authInfo.playerOne, + playerTwo = authInfo.playerTwo + ) + ) + } +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameView.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameView.kt new file mode 100644 index 000000000..56769981e --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameView.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.off_game + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 OffGameView(viewModel: State, eventStream: EventStream) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.Bottom), + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text(text = viewModel.value.playerOne) + Text(text = "Win Count: ${viewModel.value.playerOneWins}") + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text(text = viewModel.value.playerTwo) + Text(text = "Win Count: ${viewModel.value.playerTwoWins}") + } + CustomButton( + analyticsId = "26882559-fc45", + onClick = { eventStream.notify(OffGameEvent.StartGame) }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "START GAME") + } + } +} + +@Preview +@Composable +fun OffGameViewPreview() { + val viewModel = remember { mutableStateOf(OffGameViewModel("James", "Alejandro", 3, 0)) } + OffGameView(viewModel, EventStream()) +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameViewModel.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameViewModel.kt new file mode 100644 index 000000000..4ed085b19 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/off_game/OffGameViewModel.kt @@ -0,0 +1,23 @@ +/* + * 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.off_game + +data class OffGameViewModel( + val playerOne: String = "", + val playerTwo: String = "", + val playerOneWins: Int = 0, + val playerTwoWins: Int = 0 +) diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/Board.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/Board.kt new file mode 100644 index 000000000..b18668d95 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/Board.kt @@ -0,0 +1,64 @@ +/* + * 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.tic_tac_toe + +import javax.inject.Inject + +class Board @Inject constructor() { + var cells: Array> + var currentRow = 0 + var currentCol = 0 + + /** Return true if it is a draw (i.e., no more EMPTY cell) */ + fun isDraw(): Boolean { + for (row in 0 until ROWS) { + for (col in 0 until COLS) { + if (cells[row][col] == null) { + return false + } + } + } + return !hasWon(MarkerType.CROSS) && !hasWon(MarkerType.NOUGHT) + } + + /** Return true if the player with "theSeed" has won after placing at (currentRow, currentCol) */ + fun hasWon(theSeed: MarkerType): Boolean { + return ( + cells[currentRow][0] == theSeed && cells[currentRow][1] == theSeed && cells[currentRow][2] == theSeed || + cells[0][currentCol] == theSeed && cells[1][currentCol] == theSeed && cells[2][currentCol] == theSeed || + currentRow == currentCol && cells[0][0] == theSeed && cells[1][1] == theSeed && cells[2][2] == theSeed || + currentRow + currentCol == 2 && cells[0][2] == theSeed && cells[1][1] == theSeed && cells[2][0] == theSeed + ) + } + + enum class MarkerType { + CROSS, NOUGHT + } + + companion object { + const val ROWS = 3 + const val COLS = 3 + } + + init { + cells = Array(ROWS) { arrayOfNulls(COLS) } + for (row in 0 until ROWS) { + for (col in 0 until COLS) { + cells[row][col] = null + } + } + } +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/BoardCoordinate.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/BoardCoordinate.kt new file mode 100644 index 000000000..3d2748439 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/BoardCoordinate.kt @@ -0,0 +1,18 @@ +/* + * 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.tic_tac_toe + +data class BoardCoordinate(val x: Int, val y: Int) diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeEvent.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeEvent.kt new file mode 100644 index 000000000..2c8896ec3 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeEvent.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.main.logged_in.tic_tac_toe + +sealed class TicTacToeEvent { + object XpButtonClick : TicTacToeEvent() + class BoardClick(val coordinate: BoardCoordinate) : TicTacToeEvent() +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeInteractor.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeInteractor.kt new file mode 100644 index 000000000..305a65ff9 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeInteractor.kt @@ -0,0 +1,93 @@ +/* + * 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.tic_tac_toe + +import com.uber.autodispose.autoDispose +import com.uber.rib.compose.root.main.AuthInfo +import com.uber.rib.compose.util.EventStream +import com.uber.rib.compose.util.StateStream +import com.uber.rib.core.BasicInteractor +import com.uber.rib.core.Bundle +import com.uber.rib.core.ComposePresenter +import io.reactivex.rxkotlin.ofType + +class TicTacToeInteractor( + presenter: ComposePresenter, + private val authInfo: AuthInfo, + private val eventStream: EventStream, + private val stateStream: StateStream, + private val listener: Listener +) : BasicInteractor(presenter) { + + var currentPlayer: Board.MarkerType = Board.MarkerType.CROSS + + override fun didBecomeActive(savedInstanceState: Bundle?) { + super.didBecomeActive(savedInstanceState) + eventStream.observe() + .ofType() + .autoDispose(this) + .subscribe { + val board: Board = stateStream.current().board + val coord = it.coordinate + + if (board.cells[coord.x][coord.y] == null) { + if (currentPlayer == Board.MarkerType.CROSS) { + board.cells[coord.x][coord.y] = Board.MarkerType.CROSS + board.currentRow = coord.x + board.currentCol = coord.y + currentPlayer = Board.MarkerType.NOUGHT + } else { + board.cells[coord.x][coord.y] = Board.MarkerType.NOUGHT + board.currentRow = coord.x + board.currentCol = coord.y + currentPlayer = Board.MarkerType.CROSS + } + } + + if (board.hasWon(Board.MarkerType.CROSS)) { + listener.onGameWon(authInfo.playerOne) + } else if (board.hasWon(Board.MarkerType.NOUGHT)) { + listener.onGameWon(authInfo.playerTwo) + } else if (board.isDraw()) { + listener.onGameWon(null) + } + + val newPlayerName = if (currentPlayer == Board.MarkerType.CROSS) { + authInfo.playerOne + } else { + authInfo.playerTwo + } + + stateStream.dispatch( + stateStream.current().copy( + board = board, + currentPlayer = newPlayerName + ) + ) + } + + eventStream.observe() + .ofType() + .autoDispose(this) + .subscribe { + // router.goToSomewhere() + } + } + + interface Listener { + fun onGameWon(winnerName: String?) + } +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeRouter.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeRouter.kt new file mode 100644 index 000000000..fde92b5f6 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeRouter.kt @@ -0,0 +1,24 @@ +/* + * 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.tic_tac_toe + +import com.uber.rib.core.BasicComposeRouter +import com.uber.rib.core.ComposePresenter + +class TicTacToeRouter( + presenter: ComposePresenter, + interactor: TicTacToeInteractor +) : BasicComposeRouter(presenter, interactor) diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeScope.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeScope.kt new file mode 100644 index 000000000..cb77b165e --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeScope.kt @@ -0,0 +1,47 @@ +/* + * 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.tic_tac_toe + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rxjava2.subscribeAsState +import com.uber.rib.compose.root.main.AuthInfo +import com.uber.rib.compose.util.EventStream +import com.uber.rib.compose.util.StateStream +import com.uber.rib.core.ComposePresenter + +@motif.Scope +interface TicTacToeScope { + fun router(): TicTacToeRouter + + @motif.Objects + abstract class Objects { + abstract fun router(): TicTacToeRouter + + abstract fun interactor(): TicTacToeInteractor + + fun presenter(stateStream: StateStream, eventStream: EventStream): ComposePresenter { + return object : ComposePresenter() { + override val composable = @Composable { + TicTacToeView(stateStream.observe().subscribeAsState(initial = stateStream.current()), eventStream) + } + } + } + + fun eventStream() = EventStream() + + fun stateStream(authInfo: AuthInfo) = StateStream(TicTacToeViewModel(authInfo.playerOne, Board())) + } +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeView.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeView.kt new file mode 100644 index 000000000..d20e13d4d --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeView.kt @@ -0,0 +1,95 @@ +/* + * 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.tic_tac_toe + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.GridCells +import androidx.compose.foundation.lazy.LazyVerticalGrid +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.uber.rib.compose.util.EventStream + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun TicTacToeView(viewModel: State, eventStream: EventStream) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = Modifier + .fillMaxSize() + .background(Color.Blue) + ) { + Text("Current Player: ${viewModel.value.currentPlayer}", color = Color.White) + Box(modifier = Modifier.aspectRatio(1f).fillMaxSize()) { + LazyVerticalGrid(cells = GridCells.Fixed(3), modifier = Modifier.fillMaxSize()) { + val board = viewModel.value.board + items(9) { i -> + val row = i / 3 + val col = i % 3 + Text( + text = when (board.cells[row][col]) { + Board.MarkerType.CROSS -> "X" + Board.MarkerType.NOUGHT -> "O" + else -> " " + }, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .padding(16.dp) + .background(Color.LightGray) + .clickable( + enabled = board.cells[row][col] == null, + onClick = { + eventStream.notify(TicTacToeEvent.BoardClick(BoardCoordinate(row, col))) + } + ) + .padding(32.dp) + ) + } + } + } + } +} + +@Preview +@Composable +fun ProductSelectionViewPreview() { + val board = Board() + board.cells[0][2] = Board.MarkerType.CROSS + board.cells[1][0] = Board.MarkerType.NOUGHT + board.cells[2][1] = Board.MarkerType.CROSS + val viewModel = remember { mutableStateOf(TicTacToeViewModel("James", board)) } + TicTacToeView(viewModel, EventStream()) +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeViewModel.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeViewModel.kt new file mode 100644 index 000000000..38c2fcca9 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_in/tic_tac_toe/TicTacToeViewModel.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.main.logged_in.tic_tac_toe + +data class TicTacToeViewModel( + val currentPlayer: String, + val board: Board +) diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutEvent.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutEvent.kt new file mode 100644 index 000000000..a09e96162 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutEvent.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.main.logged_out + +sealed class LoggedOutEvent { + class PlayerNameChanged(val name: String, val num: Int) : LoggedOutEvent() + object LogInClick : LoggedOutEvent() +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutInteractor.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutInteractor.kt new file mode 100644 index 000000000..e3e7b45d5 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutInteractor.kt @@ -0,0 +1,59 @@ +/* + * 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_out + +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.util.EventStream +import com.uber.rib.compose.util.StateStream +import com.uber.rib.core.BasicInteractor +import com.uber.rib.core.Bundle +import com.uber.rib.core.ComposePresenter +import io.reactivex.rxkotlin.ofType + +class LoggedOutInteractor( + presenter: ComposePresenter, + private val authStream: AuthStream, + private val eventStream: EventStream, + private val stateStream: StateStream +) : BasicInteractor(presenter) { + + override fun didBecomeActive(savedInstanceState: Bundle?) { + super.didBecomeActive(savedInstanceState) + eventStream.observe() + .ofType() + .autoDispose(this) + .subscribe { + with(stateStream) { + dispatch( + current().copy( + playerOne = if (it.num == 1) it.name else current().playerOne, + playerTwo = if (it.num == 2) it.name else current().playerTwo + ) + ) + } + } + + eventStream.observe() + .ofType() + .autoDispose(this) + .subscribe { + val currentState = stateStream.current() + authStream.accept(AuthInfo(true, currentState.playerOne, currentState.playerTwo)) + } + } +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutRouter.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutRouter.kt new file mode 100644 index 000000000..53b4577d4 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutRouter.kt @@ -0,0 +1,24 @@ +/* + * 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_out + +import com.uber.rib.core.BasicComposeRouter +import com.uber.rib.core.ComposePresenter + +class LoggedOutRouter( + presenter: ComposePresenter, + interactor: LoggedOutInteractor +) : BasicComposeRouter(presenter, interactor) diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutScope.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutScope.kt new file mode 100644 index 000000000..e4848c965 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutScope.kt @@ -0,0 +1,46 @@ +/* + * 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_out + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rxjava2.subscribeAsState +import com.uber.rib.compose.util.EventStream +import com.uber.rib.compose.util.StateStream +import com.uber.rib.core.ComposePresenter + +@motif.Scope +interface LoggedOutScope { + fun router(): LoggedOutRouter + + @motif.Objects + abstract class Objects { + abstract fun router(): LoggedOutRouter + + abstract fun interactor(): LoggedOutInteractor + + fun presenter(stateStream: StateStream, eventStream: EventStream): ComposePresenter { + return object : ComposePresenter() { + override val composable = @Composable { + LoggedOutView(stateStream.observe().subscribeAsState(initial = stateStream.current()), eventStream) + } + } + } + + fun eventStream() = EventStream() + + fun stateStream() = StateStream(LoggedOutViewModel()) + } +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutView.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutView.kt new file mode 100644 index 000000000..35d78d542 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutView.kt @@ -0,0 +1,71 @@ +/* + * 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_out + +import androidx.compose.foundation.layout.Arrangement +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.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.EventStream + +@Composable +fun LoggedOutView(viewModel: State, eventStream: EventStream) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.Bottom), + ) { + TextField( + value = viewModel.value.playerOne, + onValueChange = { eventStream.notify(LoggedOutEvent.PlayerNameChanged(it, 1)) }, + placeholder = { Text(text = "Player One Name") }, + modifier = Modifier.fillMaxWidth() + ) + TextField( + value = viewModel.value.playerTwo, + onValueChange = { eventStream.notify(LoggedOutEvent.PlayerNameChanged(it, 2)) }, + placeholder = { Text(text = "Player Two Name") }, + modifier = Modifier.fillMaxWidth() + ) + Button( + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Black, contentColor = Color.White), + onClick = { eventStream.notify(LoggedOutEvent.LogInClick) }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "LOGIN") + } + } +} + +@Preview +@Composable +fun LoggedOutViewPreview() { + val viewModel = remember { mutableStateOf(LoggedOutViewModel("James", "Alejandro")) } + LoggedOutView(viewModel, EventStream()) +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutViewModel.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutViewModel.kt new file mode 100644 index 000000000..2694b04dc --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/root/main/logged_out/LoggedOutViewModel.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.main.logged_out + +data class LoggedOutViewModel( + val playerOne: String = "", + val playerTwo: String = "" +) diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/AnalyticsClient.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/AnalyticsClient.kt new file mode 100644 index 000000000..1841b1187 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/AnalyticsClient.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.util + +import android.app.Application +import android.util.Log +import android.widget.Toast + +class AnalyticsClientImpl(private val application: Application) : AnalyticsClient { + override fun trackClick(id: String) { + track(id, EventType.CLICK) + } + + override fun trackImpression(id: String) { + track(id, EventType.IMPRESSION) + } + + private fun track(id: String, type: EventType) { + val message = "$type for $id @ ${System.currentTimeMillis()}" + Toast.makeText(application.applicationContext, message, Toast.LENGTH_SHORT).show() + Log.d(this::class.java.simpleName, message) + } + + enum class EventType { CLICK, IMPRESSION } +} + +object NoOpAnalyticsClient : AnalyticsClient { + override fun trackClick(id: String) = Unit + override fun trackImpression(id: String) = Unit +} + +interface AnalyticsClient { + fun trackClick(id: String) + fun trackImpression(id: String) +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/CustomButton.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/CustomButton.kt new file mode 100644 index 000000000..c8b2ce409 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/CustomButton.kt @@ -0,0 +1,47 @@ +/* + * 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.util + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun CustomButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + analyticsId: String? = null, + content: @Composable RowScope.() -> Unit +) { + val analyticsClient = AnalyticsLocal.current + val onClickWrapper: () -> Unit = { + analyticsId?.let { analyticsClient.trackClick(it) } + onClick() + } + Button( + onClick = onClickWrapper, + modifier = modifier, + colors = ButtonDefaults.buttonColors(backgroundColor = Color.Black, contentColor = Color.White), + content = content + ) + LaunchedEffect(null) { + analyticsId?.let { analyticsClient.trackImpression(it) } + } +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/CustomClientProvider.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/CustomClientProvider.kt new file mode 100644 index 000000000..8618dfd6d --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/CustomClientProvider.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.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf + +val AnalyticsLocal = staticCompositionLocalOf { NoOpAnalyticsClient } +val ExperimentsLocal = staticCompositionLocalOf { NoOpExperimentClient } +val LoggerLocal = staticCompositionLocalOf { NoOpLoggerClient } + +@Composable +fun CustomClientProvider( + analyticsClient: AnalyticsClient, + experimentClient: ExperimentClient, + loggerClient: LoggerClient, + content: @Composable () -> Unit +) { + CompositionLocalProvider( + AnalyticsLocal provides analyticsClient, + ExperimentsLocal provides experimentClient, + LoggerLocal provides loggerClient, + content = content + ) +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/EventStream.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/EventStream.kt new file mode 100644 index 000000000..6ad7c322b --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/EventStream.kt @@ -0,0 +1,27 @@ +/* + * 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.util + +import com.jakewharton.rxrelay2.PublishRelay +import io.reactivex.Observable + +class EventStream { + private val eventRelay = PublishRelay.create() + + fun notify(event: T) = eventRelay.accept(event) + + fun observe(): Observable = eventRelay.hide() +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/ExperimentClient.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/ExperimentClient.kt new file mode 100644 index 000000000..2e9742e6b --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/ExperimentClient.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.util + +import android.app.Application +import android.util.Log +import android.widget.Toast +import java.lang.Math.random + +class ExperimentClientImpl(private val application: Application) : ExperimentClient { + override fun isTreated(id: String): Boolean { + val result = random() > 0.5 + val message = "isTreated($id) = $result" + Toast.makeText(application.applicationContext, message, Toast.LENGTH_SHORT).show() + Log.d(this::class.java.simpleName, message) + return result + } +} + +object NoOpExperimentClient : ExperimentClient { + override fun isTreated(id: String) = false +} + +interface ExperimentClient { + fun isTreated(id: String): Boolean +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/LoggerClient.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/LoggerClient.kt new file mode 100644 index 000000000..7907bf855 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/LoggerClient.kt @@ -0,0 +1,35 @@ +/* + * 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.util + +import android.app.Application +import android.util.Log +import android.widget.Toast + +class LoggerClientImpl(private val application: Application) : LoggerClient { + override fun log(message: String) { + Toast.makeText(application.applicationContext, message, Toast.LENGTH_SHORT).show() + Log.d(this::class.java.simpleName, message) + } +} + +object NoOpLoggerClient : LoggerClient { + override fun log(message: String) = Unit +} + +interface LoggerClient { + fun log(message: String) +} diff --git a/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/StateStream.kt b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/StateStream.kt new file mode 100644 index 000000000..6245fa467 --- /dev/null +++ b/android/demos/compose/src/main/kotlin/com/uber/rib/compose/util/StateStream.kt @@ -0,0 +1,29 @@ +/* + * 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.util + +import com.jakewharton.rxrelay2.BehaviorRelay +import io.reactivex.Observable + +class StateStream(default: T) { + private val stateRelay = BehaviorRelay.createDefault(default) + + fun dispatch(viewModel: T) = stateRelay.accept(viewModel) + + fun observe(): Observable = stateRelay.hide() + + fun current(): T = stateRelay.value ?: throw IllegalStateException("No state in relay") +} diff --git a/android/demos/compose/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/demos/compose/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/android/demos/compose/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/demos/compose/src/main/res/drawable/ic_launcher_background.xml b/android/demos/compose/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/android/demos/compose/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/demos/compose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/demos/compose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/android/demos/compose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/demos/compose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/demos/compose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/android/demos/compose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/demos/compose/src/main/res/mipmap-hdpi/ic_launcher.png b/android/demos/compose/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..a571e6009 Binary files /dev/null and b/android/demos/compose/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/demos/compose/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/demos/compose/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..61da551c5 Binary files /dev/null and b/android/demos/compose/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/demos/compose/src/main/res/mipmap-mdpi/ic_launcher.png b/android/demos/compose/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..c41dd2853 Binary files /dev/null and b/android/demos/compose/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/demos/compose/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/demos/compose/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..db5080a75 Binary files /dev/null and b/android/demos/compose/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/demos/compose/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/demos/compose/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..6dba46dab Binary files /dev/null and b/android/demos/compose/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/demos/compose/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/demos/compose/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..da31a871c Binary files /dev/null and b/android/demos/compose/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/demos/compose/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/demos/compose/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..15ac68172 Binary files /dev/null and b/android/demos/compose/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/demos/compose/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/demos/compose/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..b216f2d31 Binary files /dev/null and b/android/demos/compose/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/demos/compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/demos/compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..f25a41974 Binary files /dev/null and b/android/demos/compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/demos/compose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/demos/compose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..e96783ccc Binary files /dev/null and b/android/demos/compose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/demos/compose/src/main/res/values/ids.xml b/android/demos/compose/src/main/res/values/ids.xml new file mode 100644 index 000000000..21ffa0bf3 --- /dev/null +++ b/android/demos/compose/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/demos/compose/src/main/res/values/strings.xml b/android/demos/compose/src/main/res/values/strings.xml new file mode 100644 index 000000000..49fac3884 --- /dev/null +++ b/android/demos/compose/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Compose RIBs + \ No newline at end of file diff --git a/android/demos/compose/src/main/res/values/themes.xml b/android/demos/compose/src/main/res/values/themes.xml new file mode 100644 index 000000000..f113d3b9d --- /dev/null +++ b/android/demos/compose/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/android/gradle/dependencies.gradle b/android/gradle/dependencies.gradle index d3e9ae4a7..9de542138 100755 --- a/android/gradle/dependencies.gradle +++ b/android/gradle/dependencies.gradle @@ -22,6 +22,7 @@ def versions = [ percent: '1.0.0' ], autodispose: '1.4.0', + coroutines: '1.4.3', dagger: "2.34", errorProne: '2.3.3', gjf: '1.7', @@ -29,6 +30,7 @@ def versions = [ kotlin: "1.5.21", ktfmt: '0.23', ktlint: '0.41.0', + motif: '0.3.4', rave: "2.0.0", robolectric: "4.4", spotless: '5.11.0' @@ -119,14 +121,20 @@ def external = [ ] def kotlin = [ + coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}", + coroutinesAndroid: "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}", + coroutinesRx2: "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:${versions.coroutines}", stdlib: "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}" ] def uber = [ autodispose: "com.uber.autodispose:autodispose:${versions.autodispose}", - autodisposeLifecycle: "com.uber.autodispose:autodispose-lifecycle:${versions.autodispose}", autodisposeAndroid : "com.uber.autodispose:autodispose-android:${versions.autodispose}@aar", + autodisposeLifecycle: "com.uber.autodispose:autodispose-lifecycle:${versions.autodispose}", + autodisposeCoroutines: "com.uber.autodispose:autodispose-coroutines-interop:${versions.autodispose}", autodisposeErrorProne: "com.uber.autodispose:autodispose-error-prone:${versions.autodispose}", + motif: "com.uber.motif:motif:${versions.motif}", + motifCompiler: "com.uber.motif:motif-compiler:${versions.motif}", rave: "com.uber:rave:${versions.rave}", ] diff --git a/android/settings.gradle b/android/settings.gradle index 7c10dfed4..a49449744 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -22,3 +22,4 @@ include ':tutorials:tutorial4' include ':demos:flipper' include ':demos:intellij' include ':demos:memory-leaks' +include ':demos:compose'