diff --git a/dependencies.gradle b/dependencies.gradle index 74e0407d9..a0f3c66ce 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -3,6 +3,7 @@ ext { androidTestVersion = "1.4.0" androidJUnitVersion = "1.1.0" lifecycleVersion = '2.2.0' + lifecycleKtxVersion = '2.4.0-alpha03' espressoVersion = "3.4.0" junitVersion = "4.13.2" @@ -12,6 +13,7 @@ ext { robolectricVersion = "4.6.1" truthVersion = "1.1.3" composeVersion = "1.0.1" + kotlinCoroutinesVersion = "1.5.2" libraries = [ androidx : [ @@ -22,7 +24,8 @@ ext { ktx: "androidx.core:core-ktx:1.6.0" ], activity : [ - compose : "androidx.activity:activity-compose:1.3.1" + compose : "androidx.activity:activity-compose:1.3.1", + ktx : "androidx.activity:activity-ktx:1.3.1" ], compose : [ foundation : "androidx.compose.foundation:foundation:$composeVersion", @@ -38,7 +41,9 @@ ext { constraintlayout: "androidx.constraintlayout:constraintlayout:2.1.0", lifecycle : [ runtime : "androidx.lifecycle:lifecycle-runtime:$lifecycleVersion", - extensions: "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" + extensions: "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion", + runtimektx : "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleKtxVersion", + extensionsktx: "androidx.lifecycle:lifecycle-extensions-ktx:$lifecycleKtxVersion", ], recyclerview : "androidx.recyclerview:recyclerview:$androidXVersion", fragment : [ @@ -60,11 +65,14 @@ ext { ], junit : "junit:junit:$junitVersion", kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion", + coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion", kotlinReflect : "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion", robolectric : "org.robolectric:robolectric:$robolectricVersion", rxjava : "io.reactivex.rxjava3:rxjava:3.1.1", rxandroid : "io.reactivex.rxjava3:rxandroid:3.0.0", rxrelays : "com.jakewharton.rxrelay3:rxrelay:3.0.1", - truth : "com.google.truth:truth:$truthVersion" + truth : "com.google.truth:truth:$truthVersion", + coroutinesTest : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion" + ] } diff --git a/formula-coroutines/.gitignore b/formula-coroutines/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/formula-coroutines/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/formula-coroutines/build.gradle b/formula-coroutines/build.gradle new file mode 100644 index 000000000..2143f8374 --- /dev/null +++ b/formula-coroutines/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java-library' + id 'kotlin' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + implementation libraries.kotlin + implementation libraries.coroutines + + api project(":formula") + + testImplementation project(":formula-test") + testImplementation libraries.truth + testImplementation libraries.junit + testImplementation libraries.coroutinesTest +} \ No newline at end of file diff --git a/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowFormula.kt b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowFormula.kt new file mode 100644 index 000000000..c58ea6c07 --- /dev/null +++ b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowFormula.kt @@ -0,0 +1,18 @@ +package com.instacart.formula.coroutines + +import com.instacart.formula.Stream +import com.instacart.formula.StreamFormula +import kotlinx.coroutines.flow.Flow + +abstract class FlowFormula : StreamFormula() { + + abstract override fun initialValue(input: Input): Output + + abstract fun flow(input: Input): Flow + + final override fun stream(input: Input): Stream { + return FlowStream.fromFlow { + flow(input) + } + } +} \ No newline at end of file diff --git a/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowRuntime.kt b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowRuntime.kt new file mode 100644 index 000000000..0ef0315ed --- /dev/null +++ b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowRuntime.kt @@ -0,0 +1,43 @@ +package com.instacart.formula.coroutines + +import com.instacart.formula.FormulaRuntime +import com.instacart.formula.IFormula +import com.instacart.formula.internal.ThreadChecker +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged + +object FlowRuntime { + fun start( + input: Flow, + formula: IFormula + ): Flow { + val threadChecker = ThreadChecker() + return callbackFlow { + threadChecker.check("Need to subscribe on main thread.") + + + var runtime = FormulaRuntime(threadChecker, formula, ::trySendBlocking, channel::close) + + input.collect { input -> + threadChecker.check("Input arrived on a wrong thread.") + if (!runtime.isKeyValid(input)) { + runtime.terminate() + runtime = + FormulaRuntime(threadChecker, formula, ::trySendBlocking, channel::close) + } + runtime.onInput(input) + } + + awaitClose { + if(!channel.isClosedForSend) { + runtime.terminate() + } + } + + }.distinctUntilChanged() + } +} \ No newline at end of file diff --git a/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowRuntimeExtensions.kt b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowRuntimeExtensions.kt new file mode 100644 index 000000000..a619bfe1f --- /dev/null +++ b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowRuntimeExtensions.kt @@ -0,0 +1,22 @@ +package com.instacart.formula.coroutines + +import com.instacart.formula.IFormula +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + + +fun IFormula.toFlow(): Flow { + return toFlow(input = Unit) +} + +fun IFormula.toFlow( + input: Input +): Flow { + return toFlow(input = flowOf(input)) +} + +fun IFormula.toFlow( + input: Flow +): Flow { + return FlowRuntime.start(input = input, formula = this) +} diff --git a/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowStream.kt b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowStream.kt new file mode 100644 index 000000000..c34b74ce0 --- /dev/null +++ b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowStream.kt @@ -0,0 +1,80 @@ +package com.instacart.formula.coroutines + +import com.instacart.formula.Cancelable +import com.instacart.formula.Stream +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +/** + * Formula [Stream] adapter to enable coroutine's Flow use. + */ +interface FlowStream : Stream { + + companion object { + /** + * Creates a [Stream] from a [Flow] factory [create]. + * + * ``` + * events(FlowStream.fromFlow { locationManager.updates() }) { event -> + * transition() + * } + * ``` + */ + inline fun fromFlow( + scope: CoroutineScope = MainScope(), + crossinline create: () -> Flow + ): Stream { + return object : FlowStream { + + override val scope: CoroutineScope = scope + + override fun flow(): Flow { + return create() + } + + override fun key(): Any = Unit + } + } + + /** + * Creates a [Stream] from a [Flow] factory [create]. + * + * ``` + * events(FlowStream.fromFlow(itemId) { repo.fetchItem(itemId) }) { event -> + * transition() + * } + * ``` + * + * @param key Used to distinguish this [Stream] from other streams. + */ + inline fun fromFlow( + scope: CoroutineScope = MainScope(), + key: Any?, + crossinline create: () -> Flow + ): Stream { + return object : FlowStream { + override val scope: CoroutineScope = scope + + override fun flow(): Flow { + return create() + } + + override fun key(): Any? = key + } + } + } + + fun flow(): Flow + + val scope: CoroutineScope + + override fun start(send: (Message) -> Unit): Cancelable? { + val job = flow() + .onEach { send(it) } + .launchIn(scope) + return Cancelable(job::cancel) + } +} \ No newline at end of file diff --git a/formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTest.kt b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTest.kt new file mode 100644 index 000000000..ec471efe7 --- /dev/null +++ b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTest.kt @@ -0,0 +1,19 @@ +package com.instacart.formula.coroutines + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Rule + +@ExperimentalCoroutinesApi +interface CoroutineTest { + + @ExperimentalStdlibApi + @get:Rule + val coroutineRule: CoroutineTestRule + + @ExperimentalStdlibApi + fun test( + test: suspend TestCoroutineScope.() -> Unit) = coroutineRule.testCoroutineScope.runBlockingTest { test(coroutineRule.testCoroutineScope) } + +} \ No newline at end of file diff --git a/formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTestRule.kt b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTestRule.kt new file mode 100644 index 000000000..3965def0f --- /dev/null +++ b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTestRule.kt @@ -0,0 +1,26 @@ +package com.instacart.formula.coroutines + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +@ExperimentalStdlibApi +@ExperimentalCoroutinesApi +class CoroutineTestRule(val testCoroutineScope: TestCoroutineScope = TestCoroutineScope(TestCoroutineDispatcher())) : TestWatcher() { + init { + Dispatchers.setMain(testCoroutineScope.coroutineContext[CoroutineDispatcher]!!) + + } + + override fun finished(description: Description?) { + super.finished(description) + Dispatchers.resetMain() + testCoroutineScope.cleanupTestCoroutines() + } +} \ No newline at end of file diff --git a/formula-coroutines/src/test/java/com/instacart/formula/coroutines/FlowFormulaTest.kt b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/FlowFormulaTest.kt new file mode 100644 index 000000000..aed58020f --- /dev/null +++ b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/FlowFormulaTest.kt @@ -0,0 +1,61 @@ +package com.instacart.formula.coroutines + +import com.google.common.truth.Truth +import com.instacart.formula.test.test +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.* +import org.junit.Rule +import org.junit.Test + +@ExperimentalStdlibApi +class FlowFormulaTest : CoroutineTest { + + @get:Rule + override val coroutineRule = CoroutineTestRule() + + @Test + fun `initial value`() = test { + TestFlowFormula() + .test("initial", this) + .apply { + Truth.assertThat(values()).containsExactly(0).inOrder() + } + } + + @Test + fun `initial value and subsequent events from relay`() = test { + TestFlowFormula() + .test("initial", this) + .apply { + formula.sharedFlow.tryEmit(1) + formula.sharedFlow.tryEmit(2) + formula.sharedFlow.tryEmit(3) + } + .apply { + Truth.assertThat(values()).containsExactly(0, 1, 2, 3).inOrder() + } + } + + @Test + fun `new input restarts formula`() = test { + TestFlowFormula() + .test(this) + .input("initial") + .apply { formula.sharedFlow.tryEmit(1) } + .input("reset") + .apply { formula.sharedFlow.tryEmit(1) } + .apply { + Truth.assertThat(values()).containsExactly(0, 1, 0, 1).inOrder() + } + } + + internal class TestFlowFormula : FlowFormula() { + + val sharedFlow = + MutableSharedFlow(0, extraBufferCapacity = 1, BufferOverflow.DROP_OLDEST) + + override fun initialValue(input: String): Int = 0 + + override fun flow(input: String): Flow = sharedFlow.asSharedFlow() + } +} diff --git a/formula-test/build.gradle b/formula-test/build.gradle index 0dda0fe04..8c8d8e8c3 100644 --- a/formula-test/build.gradle +++ b/formula-test/build.gradle @@ -18,7 +18,10 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { dependencies { implementation libraries.kotlin + implementation libraries.coroutines + api project(":formula-rxjava3") + api project(":formula-coroutines") testImplementation libraries.truth testImplementation libraries.junit diff --git a/formula-test/src/main/java/com/instacart/formula/test/TestFlow.kt b/formula-test/src/main/java/com/instacart/formula/test/TestFlow.kt new file mode 100644 index 000000000..fd81d4c7d --- /dev/null +++ b/formula-test/src/main/java/com/instacart/formula/test/TestFlow.kt @@ -0,0 +1,47 @@ +package com.instacart.formula.test + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn + +class TestFlow(private val upstream: Flow) : Flow by upstream { + private val values = mutableListOf() + private val errors = mutableListOf() + + fun values(): List = values.toList() + + fun assertNoErrors() = run { + check(errors.isEmpty()) { "There are ${errors.size} errors" } + this + } + + private val internalFlow by lazy { + callbackFlow { + upstream + .catch { + errors.add(it) + } + .collect { + values.add(it) + trySend(it) + } + + awaitClose { + cancel() + } + } + } + fun test() = internalFlow + +} + +fun Flow.test(scope: CoroutineScope) : TestFlow { + val testFlow = TestFlow(this) + testFlow.test().launchIn(scope) + return testFlow +} \ No newline at end of file diff --git a/formula-test/src/main/java/com/instacart/formula/test/TestFormulaFlow.kt b/formula-test/src/main/java/com/instacart/formula/test/TestFormulaFlow.kt new file mode 100644 index 000000000..2e1b46a50 --- /dev/null +++ b/formula-test/src/main/java/com/instacart/formula/test/TestFormulaFlow.kt @@ -0,0 +1,60 @@ +package com.instacart.formula.test + +import com.instacart.formula.IFormula +import com.instacart.formula.coroutines.toFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged + +class TestFormulaFlow>( + scope: CoroutineScope, + val formula: FormulaT +) { + + private var started: Boolean = false + private val inputFlow = + MutableSharedFlow(1) + private val testFlow = formula + .toFlow(inputFlow.distinctUntilChanged()) + .test(scope) + .assertNoErrors() + + fun values(): List { + return testFlow.values() + } + + /** + * Passes input to [formula]. + */ + fun input(value: Input) = apply { + started = true + assertNoErrors() // Check before interaction + inputFlow.tryEmit(value) + assertNoErrors() // Check after interaction + } + + inline fun output(assert: Output.() -> Unit) = apply { + ensureFormulaIsRunning() + assertNoErrors() // Check before interaction + assert(values().last()) + assertNoErrors() // Check after interaction + } + + fun assertOutputCount(count: Int) = apply { + ensureFormulaIsRunning() + assertNoErrors() + val size = values().size + assert(size == count) { + "Expected: $count, was: $size" + } + } + + fun assertNoErrors() = apply { + testFlow.assertNoErrors() + } + + @PublishedApi + internal fun ensureFormulaIsRunning() { + if (!started) throw IllegalStateException("Formula is not running. Call [TeatFormulaObserver.input] to start it.") + } +} diff --git a/formula-test/src/main/java/com/instacart/formula/test/TestRuntimeExtensions.kt b/formula-test/src/main/java/com/instacart/formula/test/TestRuntimeExtensions.kt index 2df9266b5..24fd933e9 100644 --- a/formula-test/src/main/java/com/instacart/formula/test/TestRuntimeExtensions.kt +++ b/formula-test/src/main/java/com/instacart/formula/test/TestRuntimeExtensions.kt @@ -2,6 +2,7 @@ package com.instacart.formula.test import com.instacart.formula.IFormula import com.instacart.formula.Stream +import kotlinx.coroutines.CoroutineScope /** * An extension function to create a [TestFormulaObserver] for a [IFormula] instance. @@ -12,6 +13,16 @@ fun > F.test(): TestFormul return TestFormulaObserver(this) } +/** + * An extension function to create a [TestFormulaFlow] for a [IFormula] instance. + * + * Note: Formula won't start until you pass it an [input][TestFormulaFlow.input]. + */ + +fun > F.test(scope: CoroutineScope): TestFormulaFlow { + return TestFormulaFlow(scope, this) +} + /** * An extension function to create a [TestFormulaObserver] for a [IFormula] instance. * @@ -25,4 +36,18 @@ fun > F.test( } } +/** + * An extension function to create a [TestFormulaFlow] for a [IFormula] instance. + * + * @param initialInput Input passed to [IFormula]. + */ +fun > F.test( + initialInput: Input, + scope: CoroutineScope +): TestFormulaFlow { + return test(scope).apply { + input(initialInput) + } +} + fun Stream.test() = TestStreamObserver(this) diff --git a/samples/counter-coroutines/.gitignore b/samples/counter-coroutines/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/samples/counter-coroutines/.gitignore @@ -0,0 +1 @@ +/build diff --git a/samples/counter-coroutines/build.gradle b/samples/counter-coroutines/build.gradle new file mode 100644 index 000000000..2ea4b76ab --- /dev/null +++ b/samples/counter-coroutines/build.gradle @@ -0,0 +1,56 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: "kotlin-parcelize" + +android { + compileSdkVersion constants.compileSdk + defaultConfig { + applicationId "com.instacart.formula.counter.coroutines" + minSdkVersion constants.minSdk + targetSdkVersion constants.targetSdk + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation project(":formula-coroutines") + + implementation libraries.kotlin + implementation libraries.androidx.appcompat + implementation libraries.androidx.lifecycle.runtimektx + + testImplementation libraries.junit + testImplementation "com.google.truth:truth:$truthVersion" + + testImplementation libraries.androidx.test.rules + testImplementation libraries.androidx.test.runner + testImplementation libraries.androidx.test.espresso.core + testImplementation libraries.robolectric + testImplementation project(":formula-test") +} diff --git a/samples/counter-coroutines/proguard-rules.pro b/samples/counter-coroutines/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/samples/counter-coroutines/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/samples/counter-coroutines/src/main/AndroidManifest.xml b/samples/counter-coroutines/src/main/AndroidManifest.xml new file mode 100644 index 000000000..1793d76ff --- /dev/null +++ b/samples/counter-coroutines/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterActivity.kt b/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterActivity.kt new file mode 100644 index 000000000..d7875f458 --- /dev/null +++ b/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterActivity.kt @@ -0,0 +1,33 @@ +package com.instacart.formula.counter + +import android.os.Bundle +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.instacart.formula.coroutines.toFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +class CounterActivity : FragmentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.counter_activity) + + val renderView = CounterRenderView(findViewById(R.id.activity_content)) + + val formula = CounterFormula() + + formula.toFlow().safeCollect { renderView.render(it) } + } + + fun Flow.safeCollect(block: (T) -> Unit) = lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + this@safeCollect.collect { + block(it) + } + } + } +} diff --git a/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterFormula.kt b/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterFormula.kt new file mode 100644 index 000000000..9c2d45582 --- /dev/null +++ b/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterFormula.kt @@ -0,0 +1,28 @@ +package com.instacart.formula.counter + +import com.instacart.formula.Evaluation +import com.instacart.formula.Formula +import com.instacart.formula.FormulaContext + +class CounterFormula : Formula { + + override fun initialState(input: Unit): Int = 0 + + override fun evaluate( + input: Unit, + state: Int, + context: FormulaContext + ): Evaluation { + return Evaluation( + output = CounterRenderModel( + count = "Count: $state", + onDecrement = context.callback { + transition(state - 1) + }, + onIncrement = context.callback { + transition(state + 1) + } + ) + ) + } +} diff --git a/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterRenderModel.kt b/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterRenderModel.kt new file mode 100644 index 000000000..3e2754fc6 --- /dev/null +++ b/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterRenderModel.kt @@ -0,0 +1,7 @@ +package com.instacart.formula.counter + +data class CounterRenderModel( + val count: String, + val onDecrement: () -> Unit, + val onIncrement: () -> Unit +) diff --git a/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterRenderView.kt b/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterRenderView.kt new file mode 100644 index 000000000..1e2f21ecf --- /dev/null +++ b/samples/counter-coroutines/src/main/java/com/instacart/formula/counter/CounterRenderView.kt @@ -0,0 +1,23 @@ +package com.instacart.formula.counter + +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import com.instacart.formula.Renderer +import com.instacart.formula.RenderView + +class CounterRenderView(private val root: ViewGroup): RenderView { + private val decrementButton: Button = root.findViewById(R.id.decrement_button) + private val incrementButton: Button = root.findViewById(R.id.increment_button) + private val countTextView: TextView = root.findViewById(R.id.count_text_view) + + override val render: Renderer = Renderer { model -> + countTextView.text = model.count + decrementButton.setOnClickListener { + model.onDecrement() + } + incrementButton.setOnClickListener { + model.onIncrement() + } + } +} diff --git a/samples/counter-coroutines/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/counter-coroutines/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..c7bd21dbd --- /dev/null +++ b/samples/counter-coroutines/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/samples/counter-coroutines/src/main/res/drawable/ic_launcher_background.xml b/samples/counter-coroutines/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..2408e30d1 --- /dev/null +++ b/samples/counter-coroutines/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/counter-coroutines/src/main/res/layout/counter_activity.xml b/samples/counter-coroutines/src/main/res/layout/counter_activity.xml new file mode 100644 index 000000000..caffcb41f --- /dev/null +++ b/samples/counter-coroutines/src/main/res/layout/counter_activity.xml @@ -0,0 +1,35 @@ + + + + + +